use crate::contour::{ContourId, ContourType, CutContour};
#[derive(Debug, Clone)]
pub struct CommonEdge {
pub contour_a: ContourId,
pub edge_a: usize,
pub contour_b: ContourId,
pub edge_b: usize,
pub overlap_length: f64,
pub midpoint: (f64, f64),
}
#[derive(Debug, Clone)]
pub struct CommonEdgeResult {
pub common_edges: Vec<CommonEdge>,
pub total_common_length: f64,
}
pub fn detect_common_edges(
contours: &[CutContour],
max_distance: f64,
angle_tolerance: f64,
) -> CommonEdgeResult {
let mut common_edges = Vec::new();
let exteriors: Vec<&CutContour> = contours
.iter()
.filter(|c| c.contour_type == ContourType::Exterior)
.collect();
for i in 0..exteriors.len() {
for j in (i + 1)..exteriors.len() {
let ca = exteriors[i];
let cb = exteriors[j];
if !aabb_proximity(ca, cb, max_distance) {
continue;
}
detect_edges_between(ca, cb, max_distance, angle_tolerance, &mut common_edges);
}
}
let total_common_length: f64 = common_edges.iter().map(|e| e.overlap_length).sum();
CommonEdgeResult {
common_edges,
total_common_length,
}
}
fn aabb_proximity(a: &CutContour, b: &CutContour, max_dist: f64) -> bool {
let (a_min, a_max) = contour_aabb(a);
let (b_min, b_max) = contour_aabb(b);
a_min.0 - max_dist <= b_max.0
&& a_max.0 + max_dist >= b_min.0
&& a_min.1 - max_dist <= b_max.1
&& a_max.1 + max_dist >= b_min.1
}
fn contour_aabb(c: &CutContour) -> ((f64, f64), (f64, f64)) {
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for &(x, y) in &c.vertices {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
((min_x, min_y), (max_x, max_y))
}
fn detect_edges_between(
ca: &CutContour,
cb: &CutContour,
max_distance: f64,
angle_tolerance: f64,
results: &mut Vec<CommonEdge>,
) {
let na = ca.vertices.len();
let nb = cb.vertices.len();
for i in 0..na {
let a0 = ca.vertices[i];
let a1 = ca.vertices[(i + 1) % na];
let (da_x, da_y) = (a1.0 - a0.0, a1.1 - a0.1);
let len_a = (da_x * da_x + da_y * da_y).sqrt();
if len_a < 1e-12 {
continue; }
let dir_a = (da_x / len_a, da_y / len_a);
for j in 0..nb {
let b0 = cb.vertices[j];
let b1 = cb.vertices[(j + 1) % nb];
let (db_x, db_y) = (b1.0 - b0.0, b1.1 - b0.1);
let len_b = (db_x * db_x + db_y * db_y).sqrt();
if len_b < 1e-12 {
continue;
}
let dir_b = (db_x / len_b, db_y / len_b);
let dot = dir_a.0 * dir_b.0 + dir_a.1 * dir_b.1;
if dot > -1.0 + angle_tolerance {
continue; }
let perp_dist = perpendicular_distance(a0, dir_a, b0);
if perp_dist > max_distance {
continue;
}
let overlap = compute_overlap(a0, a1, b0, b1, dir_a);
if overlap <= 1e-6 {
continue; }
let t_a0 = project_onto_line(a0, dir_a, b0);
let t_a1 = project_onto_line(a0, dir_a, b1);
let t_b_min = t_a0.min(t_a1).max(0.0);
let t_b_max = t_a0.max(t_a1).min(len_a);
let t_mid = (t_b_min + t_b_max) / 2.0;
let midpoint = (a0.0 + dir_a.0 * t_mid, a0.1 + dir_a.1 * t_mid);
results.push(CommonEdge {
contour_a: ca.id,
edge_a: i,
contour_b: cb.id,
edge_b: j,
overlap_length: overlap,
midpoint,
});
}
}
}
fn perpendicular_distance(line_point: (f64, f64), line_dir: (f64, f64), point: (f64, f64)) -> f64 {
let dx = point.0 - line_point.0;
let dy = point.1 - line_point.1;
(dx * line_dir.1 - dy * line_dir.0).abs()
}
fn project_onto_line(line_origin: (f64, f64), line_dir: (f64, f64), point: (f64, f64)) -> f64 {
let dx = point.0 - line_origin.0;
let dy = point.1 - line_origin.1;
dx * line_dir.0 + dy * line_dir.1
}
fn compute_overlap(
a0: (f64, f64),
a1: (f64, f64),
b0: (f64, f64),
b1: (f64, f64),
dir_a: (f64, f64),
) -> f64 {
let len_a = ((a1.0 - a0.0).powi(2) + (a1.1 - a0.1).powi(2)).sqrt();
let t_b0 = project_onto_line(a0, dir_a, b0);
let t_b1 = project_onto_line(a0, dir_a, b1);
let b_min = t_b0.min(t_b1);
let b_max = t_b0.max(t_b1);
let overlap_start = b_min.max(0.0);
let overlap_end = b_max.min(len_a);
(overlap_end - overlap_start).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rect(id: usize, x: f64, y: f64, w: f64, h: f64) -> CutContour {
CutContour {
id,
geometry_id: format!("part{}", id),
instance: 0,
contour_type: ContourType::Exterior,
vertices: vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)],
perimeter: 2.0 * (w + h),
centroid: (x + w / 2.0, y + h / 2.0),
}
}
#[test]
fn test_adjacent_rectangles_share_edge() {
let contours = vec![
make_rect(0, 0.0, 0.0, 10.0, 10.0),
make_rect(1, 10.2, 0.0, 10.0, 10.0), ];
let result = detect_common_edges(&contours, 0.5, 0.1);
assert!(
!result.common_edges.is_empty(),
"Should detect common edge between adjacent rectangles"
);
assert!(result.total_common_length > 9.0); }
#[test]
fn test_distant_rectangles_no_common() {
let contours = vec![
make_rect(0, 0.0, 0.0, 10.0, 10.0),
make_rect(1, 50.0, 0.0, 10.0, 10.0), ];
let result = detect_common_edges(&contours, 0.5, 0.1);
assert!(result.common_edges.is_empty());
}
#[test]
fn test_perpendicular_edges_no_common() {
let contours = vec![
make_rect(0, 0.0, 0.0, 10.0, 10.0),
make_rect(1, 10.0, 10.0, 10.0, 10.0), ];
let result = detect_common_edges(&contours, 0.5, 0.1);
for ce in &result.common_edges {
assert!(ce.overlap_length > 0.0);
}
}
#[test]
fn test_interior_contours_ignored() {
let mut contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
contours.push(CutContour {
id: 1,
geometry_id: "part1".to_string(),
instance: 0,
contour_type: ContourType::Interior, vertices: vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)],
perimeter: 24.0,
centroid: (5.0, 5.0),
});
let result = detect_common_edges(&contours, 0.5, 0.1);
assert!(
result.common_edges.is_empty(),
"Interior contours should not participate"
);
}
#[test]
fn test_stacked_rectangles_horizontal() {
let contours = vec![
make_rect(0, 0.0, 0.0, 10.0, 5.0),
make_rect(1, 0.0, 5.3, 10.0, 5.0),
];
let result = detect_common_edges(&contours, 0.5, 0.1);
assert!(!result.common_edges.is_empty());
assert!(result.total_common_length > 9.0);
}
#[test]
fn test_common_edge_result_fields() {
let contours = vec![
make_rect(0, 0.0, 0.0, 10.0, 10.0),
make_rect(1, 10.1, 0.0, 10.0, 10.0),
];
let result = detect_common_edges(&contours, 0.5, 0.1);
assert!(!result.common_edges.is_empty());
let ce = &result.common_edges[0];
assert!(ce.contour_a == 0 || ce.contour_a == 1);
assert!(ce.contour_b == 0 || ce.contour_b == 1);
assert_ne!(ce.contour_a, ce.contour_b);
assert!(ce.overlap_length > 0.0);
}
#[test]
fn test_empty_contours() {
let result = detect_common_edges(&[], 0.5, 0.1);
assert!(result.common_edges.is_empty());
assert_eq!(result.total_common_length, 0.0);
}
}