1#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21
22use crate::contour::CutContour;
23use crate::cost::point_distance;
24
25#[derive(Debug, Clone, Copy)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct BridgeConfig {
29 pub bridge_width: f64,
31 pub max_spacing: f64,
34 pub min_bridges: usize,
36 pub corner_clearance: f64,
39 pub enabled: bool,
41}
42
43impl Default for BridgeConfig {
44 fn default() -> Self {
45 Self {
46 bridge_width: 2.0,
47 max_spacing: 50.0,
48 min_bridges: 2,
49 corner_clearance: 5.0,
50 enabled: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct Bridge {
58 pub start: (f64, f64),
60 pub end: (f64, f64),
62 pub midpoint: (f64, f64),
64 pub edge_index: usize,
66 pub edge_t: f64,
68 pub perimeter_distance: f64,
70}
71
72#[derive(Debug, Clone)]
74pub struct BridgePlacement {
75 pub contour_id: usize,
77 pub bridges: Vec<Bridge>,
79}
80
81pub fn place_bridges(contour: &CutContour, config: &BridgeConfig) -> BridgePlacement {
86 if !config.enabled {
87 return BridgePlacement {
88 contour_id: contour.id,
89 bridges: Vec::new(),
90 };
91 }
92
93 let vertices = &contour.vertices;
94 let nv = vertices.len();
95 if nv < 3 {
96 return BridgePlacement {
97 contour_id: contour.id,
98 bridges: Vec::new(),
99 };
100 }
101
102 let mut edge_lengths = Vec::with_capacity(nv);
104 let mut cumulative = Vec::with_capacity(nv + 1);
105 cumulative.push(0.0);
106
107 for i in 0..nv {
108 let j = (i + 1) % nv;
109 let len = point_distance(vertices[i], vertices[j]);
110 edge_lengths.push(len);
111 cumulative.push(cumulative[i] + len);
112 }
113
114 let perimeter = cumulative[nv];
115 if perimeter < config.bridge_width * 2.0 {
116 return BridgePlacement {
117 contour_id: contour.id,
118 bridges: Vec::new(),
119 };
120 }
121
122 let n_bridges = compute_bridge_count(perimeter, config);
124
125 let spacing = perimeter / n_bridges as f64;
127 let half_width = config.bridge_width / 2.0;
128
129 let mut bridges = Vec::with_capacity(n_bridges);
130
131 for k in 0..n_bridges {
132 let target_dist = k as f64 * spacing + spacing / 2.0; let target_dist = target_dist % perimeter;
134
135 if !is_clear_of_corners(
137 target_dist,
138 half_width,
139 &cumulative,
140 config.corner_clearance,
141 ) {
142 if let Some(adjusted) = find_valid_position(
144 target_dist,
145 half_width,
146 &cumulative,
147 config.corner_clearance,
148 perimeter,
149 ) {
150 if let Some(bridge) = create_bridge(
151 adjusted,
152 half_width,
153 vertices,
154 &cumulative,
155 &edge_lengths,
156 perimeter,
157 ) {
158 bridges.push(bridge);
159 }
160 }
161 continue;
162 }
163
164 if let Some(bridge) = create_bridge(
165 target_dist,
166 half_width,
167 vertices,
168 &cumulative,
169 &edge_lengths,
170 perimeter,
171 ) {
172 bridges.push(bridge);
173 }
174 }
175
176 BridgePlacement {
177 contour_id: contour.id,
178 bridges,
179 }
180}
181
182fn compute_bridge_count(perimeter: f64, config: &BridgeConfig) -> usize {
184 let count_from_spacing = (perimeter / config.max_spacing).ceil() as usize;
185 count_from_spacing.max(config.min_bridges)
186}
187
188fn is_clear_of_corners(
190 center_dist: f64,
191 half_width: f64,
192 cumulative: &[f64],
193 clearance: f64,
194) -> bool {
195 let bridge_start = center_dist - half_width;
196 let bridge_end = center_dist + half_width;
197
198 for &vertex_dist in cumulative.iter() {
200 let dist_to_start = (vertex_dist - bridge_start).abs();
201 let dist_to_end = (vertex_dist - bridge_end).abs();
202 let dist_to_center = (vertex_dist - center_dist).abs();
203
204 if dist_to_center < clearance + half_width
205 && (dist_to_start < clearance || dist_to_end < clearance)
206 {
207 return false;
208 }
209 }
210
211 true
212}
213
214fn find_valid_position(
216 target: f64,
217 half_width: f64,
218 cumulative: &[f64],
219 clearance: f64,
220 perimeter: f64,
221) -> Option<f64> {
222 let step = clearance / 2.0;
224 for i in 1..=10 {
225 let offset = i as f64 * step;
226
227 let pos_plus = (target + offset) % perimeter;
228 if is_clear_of_corners(pos_plus, half_width, cumulative, clearance) {
229 return Some(pos_plus);
230 }
231
232 let pos_minus = (target - offset + perimeter) % perimeter;
233 if is_clear_of_corners(pos_minus, half_width, cumulative, clearance) {
234 return Some(pos_minus);
235 }
236 }
237
238 None
239}
240
241fn create_bridge(
243 center_dist: f64,
244 half_width: f64,
245 vertices: &[(f64, f64)],
246 cumulative: &[f64],
247 edge_lengths: &[f64],
248 perimeter: f64,
249) -> Option<Bridge> {
250 let start_dist = (center_dist - half_width + perimeter) % perimeter;
251 let end_dist = (center_dist + half_width) % perimeter;
252
253 let start = point_at_perimeter_distance(start_dist, vertices, cumulative, edge_lengths)?;
254 let end = point_at_perimeter_distance(end_dist, vertices, cumulative, edge_lengths)?;
255 let (mid, edge_index) =
256 point_at_perimeter_distance_with_edge(center_dist, vertices, cumulative, edge_lengths)?;
257
258 let edge_len = edge_lengths[edge_index];
259 let edge_t = if edge_len > 1e-12 {
260 (center_dist - cumulative[edge_index]) / edge_len
261 } else {
262 0.0
263 };
264
265 Some(Bridge {
266 start,
267 end,
268 midpoint: mid,
269 edge_index,
270 edge_t,
271 perimeter_distance: center_dist,
272 })
273}
274
275fn point_at_perimeter_distance(
277 dist: f64,
278 vertices: &[(f64, f64)],
279 cumulative: &[f64],
280 edge_lengths: &[f64],
281) -> Option<(f64, f64)> {
282 point_at_perimeter_distance_with_edge(dist, vertices, cumulative, edge_lengths)
283 .map(|(pt, _)| pt)
284}
285
286fn point_at_perimeter_distance_with_edge(
288 dist: f64,
289 vertices: &[(f64, f64)],
290 cumulative: &[f64],
291 edge_lengths: &[f64],
292) -> Option<((f64, f64), usize)> {
293 let nv = vertices.len();
294 let perimeter = cumulative[nv];
295 if perimeter < 1e-12 {
296 return None;
297 }
298
299 let d = dist % perimeter;
300
301 for i in 0..nv {
302 if d >= cumulative[i] && d <= cumulative[i + 1] + 1e-12 {
303 let edge_len = edge_lengths[i];
304 if edge_len < 1e-12 {
305 return Some((vertices[i], i));
306 }
307 let t = (d - cumulative[i]) / edge_len;
308 let t = t.clamp(0.0, 1.0);
309 let j = (i + 1) % nv;
310 let px = vertices[i].0 + t * (vertices[j].0 - vertices[i].0);
311 let py = vertices[i].1 + t * (vertices[j].1 - vertices[i].1);
312 return Some(((px, py), i));
313 }
314 }
315
316 Some((vertices[nv - 1], nv - 1))
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::contour::ContourType;
323
324 fn make_rect(id: usize, w: f64, h: f64) -> CutContour {
325 CutContour {
326 id,
327 geometry_id: format!("part{}", id),
328 instance: 0,
329 contour_type: ContourType::Exterior,
330 vertices: vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)],
331 perimeter: 2.0 * (w + h),
332 centroid: (w / 2.0, h / 2.0),
333 }
334 }
335
336 #[test]
337 fn test_disabled_no_bridges() {
338 let contour = make_rect(0, 100.0, 50.0);
339 let config = BridgeConfig::default(); let result = place_bridges(&contour, &config);
341 assert!(result.bridges.is_empty());
342 }
343
344 #[test]
345 fn test_enabled_places_bridges() {
346 let contour = make_rect(0, 100.0, 50.0);
347 let config = BridgeConfig {
348 enabled: true,
349 ..BridgeConfig::default()
350 };
351 let result = place_bridges(&contour, &config);
352 assert!(
353 result.bridges.len() >= config.min_bridges,
354 "Should place at least {} bridges, got {}",
355 config.min_bridges,
356 result.bridges.len()
357 );
358 }
359
360 #[test]
361 fn test_bridge_count_from_spacing() {
362 let contour = make_rect(0, 100.0, 100.0); let config = BridgeConfig {
364 enabled: true,
365 max_spacing: 50.0,
366 min_bridges: 2,
367 ..BridgeConfig::default()
368 };
369 let result = place_bridges(&contour, &config);
370 assert!(
372 result.bridges.len() >= 8,
373 "Expected >= 8 bridges for perimeter 400 with spacing 50, got {}",
374 result.bridges.len()
375 );
376 }
377
378 #[test]
379 fn test_bridge_count_min() {
380 let contour = make_rect(0, 20.0, 20.0); let config = BridgeConfig {
382 enabled: true,
383 max_spacing: 100.0, min_bridges: 3,
385 ..BridgeConfig::default()
386 };
387 let result = place_bridges(&contour, &config);
388 assert!(
389 result.bridges.len() >= 3,
390 "Should respect min_bridges=3, got {}",
391 result.bridges.len()
392 );
393 }
394
395 #[test]
396 fn test_bridge_midpoint_on_edge() {
397 let contour = make_rect(0, 100.0, 50.0);
398 let config = BridgeConfig {
399 enabled: true,
400 corner_clearance: 3.0,
401 ..BridgeConfig::default()
402 };
403 let result = place_bridges(&contour, &config);
404
405 for bridge in &result.bridges {
406 let (mx, my) = bridge.midpoint;
408 let on_boundary = (-0.1..=100.1).contains(&mx) && (-0.1..=50.1).contains(&my);
409 assert!(
410 on_boundary,
411 "Bridge midpoint ({}, {}) should be on contour boundary",
412 mx, my
413 );
414 }
415 }
416
417 #[test]
418 fn test_bridge_width() {
419 let contour = make_rect(0, 100.0, 50.0);
420 let config = BridgeConfig {
421 enabled: true,
422 bridge_width: 3.0,
423 corner_clearance: 3.0,
424 ..BridgeConfig::default()
425 };
426 let result = place_bridges(&contour, &config);
427
428 for bridge in &result.bridges {
429 let width = point_distance(bridge.start, bridge.end);
430 assert!(
432 width < config.bridge_width + 1.0,
433 "Bridge width {} should be close to {}",
434 width,
435 config.bridge_width
436 );
437 }
438 }
439
440 #[test]
441 fn test_small_contour_no_bridges() {
442 let contour = CutContour {
444 id: 0,
445 geometry_id: "tiny".to_string(),
446 instance: 0,
447 contour_type: ContourType::Exterior,
448 vertices: vec![(0.0, 0.0), (1.0, 0.0), (0.5, 0.5)],
449 perimeter: 3.0,
450 centroid: (0.5, 0.17),
451 };
452 let config = BridgeConfig {
453 enabled: true,
454 bridge_width: 2.0,
455 ..BridgeConfig::default()
456 };
457 let result = place_bridges(&contour, &config);
458 assert!(
459 result.bridges.is_empty(),
460 "Too-small contour should get no bridges"
461 );
462 }
463
464 #[test]
465 fn test_default_config() {
466 let config = BridgeConfig::default();
467 assert!(!config.enabled);
468 assert!((config.bridge_width - 2.0).abs() < 1e-10);
469 assert!((config.max_spacing - 50.0).abs() < 1e-10);
470 assert_eq!(config.min_bridges, 2);
471 assert!((config.corner_clearance - 5.0).abs() < 1e-10);
472 }
473
474 #[test]
475 fn test_bridge_edge_index_valid() {
476 let contour = make_rect(0, 100.0, 50.0);
477 let config = BridgeConfig {
478 enabled: true,
479 ..BridgeConfig::default()
480 };
481 let result = place_bridges(&contour, &config);
482
483 for bridge in &result.bridges {
484 assert!(
485 bridge.edge_index < contour.vertices.len(),
486 "Edge index {} should be < vertex count {}",
487 bridge.edge_index,
488 contour.vertices.len()
489 );
490 assert!(bridge.edge_t >= 0.0 && bridge.edge_t <= 1.0);
491 }
492 }
493}