use togo::prelude::*;
pub const MERGE_TOLERANCE: f64 = 1e-8;
#[derive(Debug, Clone)]
struct EndpointGroup {
points: Vec<Point>,
arc_indices: Vec<(usize, EndpointType)>,
centroid: Point,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum EndpointType {
Start, End, }
pub fn merge_close_endpoints(arcs: &mut Vec<Arc>, tolerance: f64) {
if arcs.is_empty() {
return;
}
let groups = find_endpoint_groups(arcs, tolerance);
merge_to_centroids(arcs, &groups);
eliminate_small_arcs(arcs, tolerance);
adjust_arcs_for_consistency(arcs);
}
fn find_endpoint_groups(arcs: &[Arc], tolerance: f64) -> Vec<EndpointGroup> {
let mut all_endpoints = Vec::new();
for (arc_idx, arc) in arcs.iter().enumerate() {
all_endpoints.push((arc.a, arc_idx, EndpointType::Start));
all_endpoints.push((arc.b, arc_idx, EndpointType::End));
}
if all_endpoints.is_empty() {
return Vec::new();
}
let mut spatial_index = HilbertRTree::with_capacity(all_endpoints.len());
for (point, _, _) in &all_endpoints {
spatial_index.add_point(point.x, point.y);
}
spatial_index.build();
let mut groups = Vec::new();
let mut used = vec![false; all_endpoints.len()];
for i in 0..all_endpoints.len() {
if used[i] {
continue;
}
let mut group = EndpointGroup {
points: Vec::new(),
arc_indices: Vec::new(),
centroid: Point::new(0.0, 0.0),
};
let (point_i, arc_i, end_type_i) = all_endpoints[i];
group.points.push(point_i);
group.arc_indices.push((arc_i, end_type_i));
used[i] = true;
let mut queue = vec![point_i];
while !queue.is_empty() {
let current_point = queue.pop().unwrap();
let mut nearby_indices = Vec::new();
spatial_index.query_circle(
current_point.x,
current_point.y,
tolerance,
&mut nearby_indices,
);
for idx in nearby_indices {
if used[idx] {
continue;
}
if let Some((x, y)) = spatial_index.get_point(idx) {
let point_j = Point::new(x, y);
let (_, arc_j, end_type_j) = all_endpoints[idx];
if (point_j - current_point).norm() <= tolerance {
group.points.push(point_j);
group.arc_indices.push((arc_j, end_type_j));
used[idx] = true;
queue.push(point_j); }
}
}
}
group.centroid = calculate_centroid(&group.points);
if group.points.len() > 1 {
groups.push(group);
}
}
groups
}
fn calculate_centroid(points: &[Point]) -> Point {
if points.is_empty() {
return Point::new(0.0, 0.0);
}
let sum = points.iter().fold(Point::new(0.0, 0.0), |acc, &p| acc + p);
sum / points.len() as f64
}
fn merge_to_centroids(arcs: &mut [Arc], groups: &[EndpointGroup]) {
for group in groups {
for &(arc_idx, endpoint_type) in &group.arc_indices {
match endpoint_type {
EndpointType::Start => {
arcs[arc_idx].a = group.centroid;
}
EndpointType::End => {
arcs[arc_idx].b = group.centroid;
}
}
}
}
}
pub fn merge_close_endpoints_default(arcs: &mut Vec<Arc>) {
merge_close_endpoints(arcs, MERGE_TOLERANCE);
}
fn eliminate_small_arcs(arcs: &mut Vec<Arc>, tolerance: f64) {
arcs.retain(|arc| !is_arc_too_small(arc, tolerance));
}
fn is_arc_too_small(arc: &Arc, tolerance: f64) -> bool {
let chord_length = (arc.b - arc.a).norm();
if arc.r == f64::INFINITY {
chord_length <= tolerance
} else {
let radius = arc.r.abs();
chord_length <= tolerance && radius <= tolerance
}
}
fn adjust_arcs_for_consistency(arcs: &mut [Arc]) {
for arc in arcs.iter_mut() {
arc.make_consistent();
}
}
#[cfg(test)]
mod tests {
use togo::prelude::*;
use super::*;
#[test]
fn test_merge_close_endpoints_simple() {
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 1e-9, 0.0 + 1e-9), point(2.0, 0.0)), ];
merge_close_endpoints(&mut arcs, 1e-8);
assert!((arcs[0].b - arcs[1].a).norm() < 1e-10);
}
#[test]
fn test_eliminate_small_line_segment() {
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0 + 1e-10, 0.0)), arcseg(point(1.0, 0.0), point(2.0, 0.0)),
];
merge_close_endpoints(&mut arcs, 1e-8);
assert_eq!(arcs.len(), 2);
}
#[test]
fn test_eliminate_small_arc() {
let mut arcs = vec![
arc(point(0.0, 0.0), point(1.0, 0.0), point(0.5, 0.5), 1.0),
arc(point(1.0, 0.0), point(1.0 + 1e-10, 1e-10), point(1.0, 1e-10), 1e-10), ];
merge_close_endpoints(&mut arcs, 1e-8);
assert_eq!(arcs.len(), 1);
}
#[test]
fn test_no_merge_needed() {
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(2.0, 0.0), point(3.0, 0.0)), ];
let original_arcs = arcs.clone();
merge_close_endpoints(&mut arcs, 1e-8);
assert_eq!(arcs.len(), 2);
for (i, arc) in arcs.iter().enumerate() {
assert!((arc.a - original_arcs[i].a).norm() < 1e-10);
assert!((arc.b - original_arcs[i].b).norm() < 1e-10);
}
}
#[test]
fn test_multiple_point_group() {
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 1e-9, 1e-9), point(1.0, 1.0)),
arcseg(point(1.0 - 1e-9, -1e-9), point(2.0, 0.0)),
];
merge_close_endpoints(&mut arcs, 1e-8);
let meeting_point = arcs[0].b;
assert!((arcs[1].a - meeting_point).norm() < 1e-10);
assert!((arcs[2].a - meeting_point).norm() < 1e-10);
}
#[test]
fn test_centroid_calculation() {
let points = vec![
point(0.0, 0.0),
point(2.0, 0.0),
point(1.0, 2.0),
];
let centroid = calculate_centroid(&points);
assert!((centroid.x - 1.0).abs() < 1e-10);
assert!((centroid.y - 2.0/3.0).abs() < 1e-10);
}
#[test]
fn test_four_arcs_common_point() {
let tolerance = 1e-8;
let common_point = point(5.0, 5.0);
let mut arcs = vec![
arcseg(point(0.0, 5.0), common_point),
arcseg(point(5.0 + 5e-9, 5.0 + 3e-9), point(10.0, 5.0)),
arcseg(point(5.0, 0.0), point(5.0 - 2e-9, 5.0 + 1e-9)),
arcseg(point(5.0 + 1e-9, 5.0 - 4e-9), point(5.0, 10.0)),
];
merge_close_endpoints(&mut arcs, tolerance);
let centroid_x = (5.0 + (5.0 + 5e-9) + (5.0 - 2e-9) + (5.0 + 1e-9)) / 4.0;
let centroid_y = (5.0 + (5.0 + 3e-9) + (5.0 + 1e-9) + (5.0 - 4e-9)) / 4.0;
assert!((arcs[0].b.x - centroid_x).abs() < 1e-15);
assert!((arcs[0].b.y - centroid_y).abs() < 1e-15);
assert!((arcs[1].a.x - centroid_x).abs() < 1e-15);
assert!((arcs[1].a.y - centroid_y).abs() < 1e-15);
assert!((arcs[2].b.x - centroid_x).abs() < 1e-15);
assert!((arcs[2].b.y - centroid_y).abs() < 1e-15);
assert!((arcs[3].a.x - centroid_x).abs() < 1e-15);
assert!((arcs[3].a.y - centroid_y).abs() < 1e-15);
}
#[test]
fn test_small_arc_elimination_in_group() {
let tolerance = 1e-8;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 2e-9, 1e-9), point(1.0 + 1e-10, 2e-10)),
arcseg(point(1.0 - 3e-9, 1e-9), point(2.0, 0.0)),
arcseg(point(10.0, 10.0), point(15.0, 10.0)),
];
let original_count = arcs.len();
merge_close_endpoints(&mut arcs, tolerance);
assert_eq!(arcs.len(), original_count - 1);
assert!((arcs[0].b - arcs[1].a).norm() < 1e-15);
}
#[test]
fn test_multiple_separate_groups() {
let tolerance = 1e-8;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 2e-9, 1e-9), point(1.5, 0.5)),
arcseg(point(1.0 - 1e-9, -2e-9), point(1.5, -0.5)),
arcseg(point(4.0, 5.0), point(5.0, 5.0)),
arcseg(point(5.0 + 3e-9, 5.0 - 1e-9), point(6.0, 5.0)),
arcseg(point(10.0, 10.0), point(15.0, 15.0)),
];
merge_close_endpoints(&mut arcs, tolerance);
let group1_centroid_x = (1.0 + (1.0 + 2e-9) + (1.0 - 1e-9)) / 3.0;
let group1_centroid_y = (0.0 + 1e-9 + (-2e-9)) / 3.0;
assert!((arcs[0].b.x - group1_centroid_x).abs() < 1e-15);
assert!((arcs[0].b.y - group1_centroid_y).abs() < 1e-15);
assert!((arcs[1].a.x - group1_centroid_x).abs() < 1e-15);
assert!((arcs[1].a.y - group1_centroid_y).abs() < 1e-15);
assert!((arcs[2].a.x - group1_centroid_x).abs() < 1e-15);
assert!((arcs[2].a.y - group1_centroid_y).abs() < 1e-15);
let group2_centroid_x = (5.0 + (5.0 + 3e-9)) / 2.0;
let group2_centroid_y = (5.0 + (5.0 - 1e-9)) / 2.0;
assert!((arcs[3].b.x - group2_centroid_x).abs() < 1e-15);
assert!((arcs[3].b.y - group2_centroid_y).abs() < 1e-15);
assert!((arcs[4].a.x - group2_centroid_x).abs() < 1e-15);
assert!((arcs[4].a.y - group2_centroid_y).abs() < 1e-15);
assert!((arcs[5].a.x - 10.0).abs() < 1e-15);
assert!((arcs[5].a.y - 10.0).abs() < 1e-15);
assert!((arcs[5].b.x - 15.0).abs() < 1e-15);
assert!((arcs[5].b.y - 15.0).abs() < 1e-15);
}
#[test]
fn test_arc_types_mixed() {
let tolerance = 1e-8;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arc(point(1.0 + 2e-9, 1e-9), point(1.5, 0.5), point(1.25, 0.25), 0.35),
arcseg(point(1.0 - 1e-9, -1e-9), point(2.0, 0.0)),
];
merge_close_endpoints(&mut arcs, tolerance);
let merged_point = arcs[0].b; assert!((arcs[1].a - merged_point).norm() < 1e-15);
assert!((arcs[2].a - merged_point).norm() < 1e-15);
assert!(arcs[0].is_seg());
assert!(arcs[1].is_arc());
assert!(arcs[2].is_seg());
}
#[test]
fn test_edge_case_same_point_exactly() {
let tolerance = 1e-8;
let exact_point = point(3.0, 4.0);
let mut arcs = vec![
arcseg(point(0.0, 0.0), exact_point),
arcseg(exact_point, point(5.0, 0.0)),
arcseg(point(2.0, 8.0), exact_point),
];
merge_close_endpoints(&mut arcs, tolerance);
assert!((arcs[0].b - exact_point).norm() < 1e-15);
assert!((arcs[1].a - exact_point).norm() < 1e-15);
assert!((arcs[2].b - exact_point).norm() < 1e-15);
}
#[test]
fn test_tolerance_boundary() {
let tolerance = 1e-3;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(0.0, 0.0), point(0.0, 1.0)),
arcseg(point(tolerance * 0.8, 0.0), point(2.0, 0.0)), arcseg(point(tolerance * 2.0, 0.0), point(3.0, 0.0)), ];
merge_close_endpoints(&mut arcs, tolerance);
assert_eq!(arcs.len(), 4); }
#[test]
fn test_very_small_arcs_elimination() {
let tolerance = 1e-8;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0 + 1e-12, 1e-12)),
arc(point(1.0 + 1e-12, 1e-12), point(1.0 + 2e-12, 0.0), point(1.0 + 1e-12, 1e-13), 1e-12),
arcseg(point(1.0 + 2e-12, 0.0), point(2.0, 0.0)),
];
let original_count = arcs.len();
merge_close_endpoints(&mut arcs, tolerance);
assert!(arcs.len() < original_count);
assert!((arcs[0].b - arcs[arcs.len()-1].a).norm() < 1e-15);
}
#[test]
fn test_complex_star_pattern() {
let tolerance = 1e-6;
let mut arcs = vec![];
for i in 0..8 {
let angle = (i as f64) * std::f64::consts::PI / 4.0;
let radius = 2.0;
let end_point = point(
radius * angle.cos(),
radius * angle.sin()
);
let offset_center = point(
(i as f64) * tolerance * 0.1 * ((i % 3) as f64 - 1.0),
(i as f64) * tolerance * 0.1 * ((i % 5) as f64 - 2.0)
);
arcs.push(arcseg(offset_center, end_point));
}
merge_close_endpoints(&mut arcs, tolerance);
assert_eq!(arcs.len(), 8);
let first_point = arcs[0].a;
let mut close_count = 0;
for arc in &arcs {
if (arc.a - first_point).norm() < tolerance {
close_count += 1;
}
}
assert!(close_count >= 2);
}
#[test]
fn test_chain_of_connections() {
let tolerance = 1e-8;
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 2e-9, 1e-9), point(2.0, 0.0)),
arcseg(point(2.0 - 1e-9, 3e-9), point(3.0, 0.0)),
arcseg(point(3.0 + 5e-9, -2e-9), point(4.0, 0.0)),
];
merge_close_endpoints(&mut arcs, tolerance);
for i in 0..arcs.len()-1 {
assert!((arcs[i].b - arcs[i+1].a).norm() < 1e-15);
}
}
#[test]
fn test_merge_endpoints_diagnostic() {
println!("Testing merge_ends module...");
let mut arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0 + 1e-9, 0.0 + 1e-9), point(2.0, 0.0)), ];
println!("Before merge:");
println!(" Arc 0: a=({:.10}, {:.10}), b=({:.10}, {:.10})",
arcs[0].a.x, arcs[0].a.y, arcs[0].b.x, arcs[0].b.y);
println!(" Arc 1: a=({:.10}, {:.10}), b=({:.10}, {:.10})",
arcs[1].a.x, arcs[1].a.y, arcs[1].b.x, arcs[1].b.y);
let distance_before = (arcs[0].b - arcs[1].a).norm();
println!(" Distance between endpoints: {:.12}", distance_before);
merge_close_endpoints(&mut arcs, 1e-8);
println!("\nAfter merge:");
println!(" Arc 0: a=({:.10}, {:.10}), b=({:.10}, {:.10})",
arcs[0].a.x, arcs[0].a.y, arcs[0].b.x, arcs[0].b.y);
if arcs.len() > 1 {
println!(" Arc 1: a=({:.10}, {:.10}), b=({:.10}, {:.10})",
arcs[1].a.x, arcs[1].a.y, arcs[1].b.x, arcs[1].b.y);
let distance_after = (arcs[0].b - arcs[1].a).norm();
println!(" Distance between endpoints: {:.12}", distance_after);
assert!(distance_after < 1e-10, "Endpoints should be very close after merge");
} else {
println!(" Only {} arc(s) remain", arcs.len());
}
println!("\nDiagnostic test completed successfully!");
}
}