1use crate::contour::{ContourId, ContourType, CutContour};
19
20#[derive(Debug, Clone)]
22pub struct CommonEdge {
23 pub contour_a: ContourId,
25 pub edge_a: usize,
27 pub contour_b: ContourId,
29 pub edge_b: usize,
31 pub overlap_length: f64,
33 pub midpoint: (f64, f64),
35}
36
37#[derive(Debug, Clone)]
39pub struct CommonEdgeResult {
40 pub common_edges: Vec<CommonEdge>,
42 pub total_common_length: f64,
44}
45
46pub fn detect_common_edges(
60 contours: &[CutContour],
61 max_distance: f64,
62 angle_tolerance: f64,
63) -> CommonEdgeResult {
64 let mut common_edges = Vec::new();
65
66 let exteriors: Vec<&CutContour> = contours
68 .iter()
69 .filter(|c| c.contour_type == ContourType::Exterior)
70 .collect();
71
72 for i in 0..exteriors.len() {
74 for j in (i + 1)..exteriors.len() {
75 let ca = exteriors[i];
76 let cb = exteriors[j];
77
78 if !aabb_proximity(ca, cb, max_distance) {
80 continue;
81 }
82
83 detect_edges_between(ca, cb, max_distance, angle_tolerance, &mut common_edges);
85 }
86 }
87
88 let total_common_length: f64 = common_edges.iter().map(|e| e.overlap_length).sum();
89
90 CommonEdgeResult {
91 common_edges,
92 total_common_length,
93 }
94}
95
96fn aabb_proximity(a: &CutContour, b: &CutContour, max_dist: f64) -> bool {
98 let (a_min, a_max) = contour_aabb(a);
99 let (b_min, b_max) = contour_aabb(b);
100
101 a_min.0 - max_dist <= b_max.0
103 && a_max.0 + max_dist >= b_min.0
104 && a_min.1 - max_dist <= b_max.1
105 && a_max.1 + max_dist >= b_min.1
106}
107
108fn contour_aabb(c: &CutContour) -> ((f64, f64), (f64, f64)) {
110 let mut min_x = f64::MAX;
111 let mut min_y = f64::MAX;
112 let mut max_x = f64::MIN;
113 let mut max_y = f64::MIN;
114
115 for &(x, y) in &c.vertices {
116 min_x = min_x.min(x);
117 min_y = min_y.min(y);
118 max_x = max_x.max(x);
119 max_y = max_y.max(y);
120 }
121
122 ((min_x, min_y), (max_x, max_y))
123}
124
125fn detect_edges_between(
127 ca: &CutContour,
128 cb: &CutContour,
129 max_distance: f64,
130 angle_tolerance: f64,
131 results: &mut Vec<CommonEdge>,
132) {
133 let na = ca.vertices.len();
134 let nb = cb.vertices.len();
135
136 for i in 0..na {
137 let a0 = ca.vertices[i];
138 let a1 = ca.vertices[(i + 1) % na];
139 let (da_x, da_y) = (a1.0 - a0.0, a1.1 - a0.1);
140 let len_a = (da_x * da_x + da_y * da_y).sqrt();
141
142 if len_a < 1e-12 {
143 continue; }
145
146 let dir_a = (da_x / len_a, da_y / len_a);
147
148 for j in 0..nb {
149 let b0 = cb.vertices[j];
150 let b1 = cb.vertices[(j + 1) % nb];
151 let (db_x, db_y) = (b1.0 - b0.0, b1.1 - b0.1);
152 let len_b = (db_x * db_x + db_y * db_y).sqrt();
153
154 if len_b < 1e-12 {
155 continue;
156 }
157
158 let dir_b = (db_x / len_b, db_y / len_b);
159
160 let dot = dir_a.0 * dir_b.0 + dir_a.1 * dir_b.1;
162 if dot > -1.0 + angle_tolerance {
163 continue; }
165
166 let perp_dist = perpendicular_distance(a0, dir_a, b0);
168 if perp_dist > max_distance {
169 continue;
170 }
171
172 let overlap = compute_overlap(a0, a1, b0, b1, dir_a);
174 if overlap <= 1e-6 {
175 continue; }
177
178 let t_a0 = project_onto_line(a0, dir_a, b0);
180 let t_a1 = project_onto_line(a0, dir_a, b1);
181 let t_b_min = t_a0.min(t_a1).max(0.0);
182 let t_b_max = t_a0.max(t_a1).min(len_a);
183 let t_mid = (t_b_min + t_b_max) / 2.0;
184 let midpoint = (a0.0 + dir_a.0 * t_mid, a0.1 + dir_a.1 * t_mid);
185
186 results.push(CommonEdge {
187 contour_a: ca.id,
188 edge_a: i,
189 contour_b: cb.id,
190 edge_b: j,
191 overlap_length: overlap,
192 midpoint,
193 });
194 }
195 }
196}
197
198fn perpendicular_distance(line_point: (f64, f64), line_dir: (f64, f64), point: (f64, f64)) -> f64 {
200 let dx = point.0 - line_point.0;
201 let dy = point.1 - line_point.1;
202 (dx * line_dir.1 - dy * line_dir.0).abs()
204}
205
206fn project_onto_line(line_origin: (f64, f64), line_dir: (f64, f64), point: (f64, f64)) -> f64 {
208 let dx = point.0 - line_origin.0;
209 let dy = point.1 - line_origin.1;
210 dx * line_dir.0 + dy * line_dir.1
211}
212
213fn compute_overlap(
215 a0: (f64, f64),
216 a1: (f64, f64),
217 b0: (f64, f64),
218 b1: (f64, f64),
219 dir_a: (f64, f64),
220) -> f64 {
221 let len_a = ((a1.0 - a0.0).powi(2) + (a1.1 - a0.1).powi(2)).sqrt();
222
223 let t_b0 = project_onto_line(a0, dir_a, b0);
225 let t_b1 = project_onto_line(a0, dir_a, b1);
226
227 let b_min = t_b0.min(t_b1);
228 let b_max = t_b0.max(t_b1);
229
230 let overlap_start = b_min.max(0.0);
232 let overlap_end = b_max.min(len_a);
233
234 (overlap_end - overlap_start).max(0.0)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn make_rect(id: usize, x: f64, y: f64, w: f64, h: f64) -> CutContour {
242 CutContour {
243 id,
244 geometry_id: format!("part{}", id),
245 instance: 0,
246 contour_type: ContourType::Exterior,
247 vertices: vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)],
248 perimeter: 2.0 * (w + h),
249 centroid: (x + w / 2.0, y + h / 2.0),
250 }
251 }
252
253 #[test]
254 fn test_adjacent_rectangles_share_edge() {
255 let contours = vec![
257 make_rect(0, 0.0, 0.0, 10.0, 10.0),
258 make_rect(1, 10.2, 0.0, 10.0, 10.0), ];
260
261 let result = detect_common_edges(&contours, 0.5, 0.1);
262
263 assert!(
265 !result.common_edges.is_empty(),
266 "Should detect common edge between adjacent rectangles"
267 );
268 assert!(result.total_common_length > 9.0); }
270
271 #[test]
272 fn test_distant_rectangles_no_common() {
273 let contours = vec![
274 make_rect(0, 0.0, 0.0, 10.0, 10.0),
275 make_rect(1, 50.0, 0.0, 10.0, 10.0), ];
277
278 let result = detect_common_edges(&contours, 0.5, 0.1);
279 assert!(result.common_edges.is_empty());
280 }
281
282 #[test]
283 fn test_perpendicular_edges_no_common() {
284 let contours = vec![
286 make_rect(0, 0.0, 0.0, 10.0, 10.0),
287 make_rect(1, 10.0, 10.0, 10.0, 10.0), ];
289
290 let result = detect_common_edges(&contours, 0.5, 0.1);
291 for ce in &result.common_edges {
295 assert!(ce.overlap_length > 0.0);
296 }
297 }
298
299 #[test]
300 fn test_interior_contours_ignored() {
301 let mut contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
302 contours.push(CutContour {
303 id: 1,
304 geometry_id: "part1".to_string(),
305 instance: 0,
306 contour_type: ContourType::Interior, vertices: vec![(2.0, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)],
308 perimeter: 24.0,
309 centroid: (5.0, 5.0),
310 });
311
312 let result = detect_common_edges(&contours, 0.5, 0.1);
313 assert!(
314 result.common_edges.is_empty(),
315 "Interior contours should not participate"
316 );
317 }
318
319 #[test]
320 fn test_stacked_rectangles_horizontal() {
321 let contours = vec![
323 make_rect(0, 0.0, 0.0, 10.0, 5.0),
324 make_rect(1, 0.0, 5.3, 10.0, 5.0),
325 ];
326
327 let result = detect_common_edges(&contours, 0.5, 0.1);
328 assert!(!result.common_edges.is_empty());
329 assert!(result.total_common_length > 9.0);
331 }
332
333 #[test]
334 fn test_common_edge_result_fields() {
335 let contours = vec![
336 make_rect(0, 0.0, 0.0, 10.0, 10.0),
337 make_rect(1, 10.1, 0.0, 10.0, 10.0),
338 ];
339
340 let result = detect_common_edges(&contours, 0.5, 0.1);
341 assert!(!result.common_edges.is_empty());
342
343 let ce = &result.common_edges[0];
344 assert!(ce.contour_a == 0 || ce.contour_a == 1);
345 assert!(ce.contour_b == 0 || ce.contour_b == 1);
346 assert_ne!(ce.contour_a, ce.contour_b);
347 assert!(ce.overlap_length > 0.0);
348 }
349
350 #[test]
351 fn test_empty_contours() {
352 let result = detect_common_edges(&[], 0.5, 0.1);
353 assert!(result.common_edges.is_empty());
354 assert_eq!(result.total_common_length, 0.0);
355 }
356}