1use std::collections::{HashMap, HashSet};
18
19use u_nesting_core::timing::Timer;
20
21use crate::common_edge::CommonEdgeResult;
22use crate::config::CuttingConfig;
23use crate::contour::CutContour;
24use crate::cost::{closest_point_on_polygon, point_distance};
25use crate::hierarchy::CuttingDag;
26use crate::pierce::{select_pierce, PierceSelection};
27
28#[derive(Debug, Clone)]
30pub struct SequenceResult {
31 pub order: Vec<usize>,
33 pub pierce_selections: Vec<PierceSelection>,
35 pub total_rapid_distance: f64,
37}
38
39pub fn optimize_sequence(
41 contours: &[CutContour],
42 dag: &CuttingDag,
43 config: &CuttingConfig,
44) -> SequenceResult {
45 optimize_sequence_with_adjacency(contours, dag, config, None)
46}
47
48pub fn optimize_sequence_with_adjacency(
55 contours: &[CutContour],
56 dag: &CuttingDag,
57 config: &CuttingConfig,
58 common_edges: Option<&CommonEdgeResult>,
59) -> SequenceResult {
60 if contours.is_empty() {
61 return SequenceResult {
62 order: Vec::new(),
63 pierce_selections: Vec::new(),
64 total_rapid_distance: 0.0,
65 };
66 }
67
68 let adjacency = build_adjacency_map(common_edges);
70
71 let index = build_contour_index(contours);
76
77 let mut order = nearest_neighbor_with_adjacency(contours, dag, config, &adjacency, &index);
79
80 if config.max_2opt_iterations > 0 {
82 improve_2opt(&mut order, dag, config, &index);
83 }
84
85 let (pierce_selections, total_rapid) = compute_pierce_selections(&order, &index, config);
87
88 SequenceResult {
89 order,
90 pierce_selections,
91 total_rapid_distance: total_rapid,
92 }
93}
94
95fn build_adjacency_map(
98 common_edges: Option<&CommonEdgeResult>,
99) -> HashMap<usize, Vec<(usize, f64)>> {
100 let mut adjacency: HashMap<usize, Vec<(usize, f64)>> = HashMap::new();
101
102 if let Some(result) = common_edges {
103 for edge in &result.common_edges {
104 adjacency
105 .entry(edge.contour_a)
106 .or_default()
107 .push((edge.contour_b, edge.overlap_length));
108 adjacency
109 .entry(edge.contour_b)
110 .or_default()
111 .push((edge.contour_a, edge.overlap_length));
112 }
113 }
114
115 adjacency
116}
117
118fn build_contour_index(contours: &[CutContour]) -> HashMap<usize, &CutContour> {
121 contours.iter().map(|c| (c.id, c)).collect()
122}
123
124fn nearest_neighbor_with_adjacency(
130 contours: &[CutContour],
131 dag: &CuttingDag,
132 config: &CuttingConfig,
133 adjacency: &HashMap<usize, Vec<(usize, f64)>>,
134 index: &HashMap<usize, &CutContour>,
135) -> Vec<usize> {
136 let n = contours.len();
137 let mut visited: HashSet<usize> = HashSet::with_capacity(n);
138 let mut order = Vec::with_capacity(n);
139 let mut current_pos = config.home_position;
140 let mut last_id: Option<usize> = None;
141
142 const ADJACENCY_DISCOUNT: f64 = 0.5;
145
146 for _ in 0..n {
147 let mut best_idx = None;
149 let mut best_score = f64::MAX;
150
151 for contour in contours.iter() {
152 if visited.contains(&contour.id) {
153 continue;
154 }
155
156 let predecessors = dag.predecessors(contour.id);
158 let ready = predecessors.iter().all(|pred_id| visited.contains(pred_id));
159
160 if !ready {
161 continue;
162 }
163
164 let dist = closest_point_on_polygon(&contour.vertices, current_pos)
166 .map(|(pt, _, _)| point_distance(current_pos, pt))
167 .unwrap_or(f64::MAX);
168
169 let mut score = dist;
171 if let Some(last) = last_id {
172 if let Some(neighbors) = adjacency.get(&last) {
173 if neighbors.iter().any(|(adj_id, _)| *adj_id == contour.id) {
174 score *= ADJACENCY_DISCOUNT;
175 }
176 }
177 }
178
179 if score < best_score {
180 best_score = score;
181 best_idx = Some(contour.id);
182 }
183 }
184
185 if let Some(id) = best_idx {
186 visited.insert(id);
187 order.push(id);
188 last_id = Some(id);
189
190 if let Some(contour) = index.get(&id) {
192 let pierce = select_pierce(contour, current_pos, config);
193 current_pos = pierce.end_point;
194 }
195 }
196 }
197
198 order
199}
200
201const TIME_CHECK_INTERVAL: u32 = 512;
207
208fn improve_2opt(
221 order: &mut [usize],
222 dag: &CuttingDag,
223 config: &CuttingConfig,
224 index: &HashMap<usize, &CutContour>,
225) {
226 let n = order.len();
227 if n < 3 {
228 return;
229 }
230
231 let timer = Timer::now();
232 let time_limited = config.time_limit_ms > 0;
233 let mut since_check: u32 = 0;
234
235 let mut improved = true;
236 let mut iterations = 0;
237 let mut current_rapid = compute_pierce_selections(order, index, config).1;
238
239 while improved && iterations < config.max_2opt_iterations {
240 improved = false;
241 iterations += 1;
242
243 for i in 0..n - 1 {
244 for j in (i + 2)..n {
245 since_check += 1;
249 if time_limited && since_check >= TIME_CHECK_INTERVAL {
250 since_check = 0;
251 if timer.elapsed_ms() >= config.time_limit_ms {
252 return;
253 }
254 }
255
256 order[i + 1..=j].reverse();
258
259 if dag.is_valid_sequence(order) {
261 let new_rapid = compute_pierce_selections(order, index, config).1;
262
263 if new_rapid < current_rapid - 1e-10 {
264 current_rapid = new_rapid;
265 improved = true;
266 } else {
267 order[i + 1..=j].reverse(); }
269 } else {
270 order[i + 1..=j].reverse(); }
272 }
273 }
274 }
275}
276
277fn compute_pierce_selections(
282 order: &[usize],
283 index: &HashMap<usize, &CutContour>,
284 config: &CuttingConfig,
285) -> (Vec<PierceSelection>, f64) {
286 let mut selections = Vec::with_capacity(order.len());
287 let mut total_rapid = 0.0;
288 let mut current_pos = config.home_position;
289
290 for &contour_id in order {
291 let contour = match index.get(&contour_id) {
292 Some(c) => c,
293 None => continue,
294 };
295
296 let pierce = select_pierce(contour, current_pos, config);
297 let rapid = point_distance(current_pos, pierce.point);
298 total_rapid += rapid;
299 current_pos = pierce.end_point;
300 selections.push(pierce);
301 }
302
303 (selections, total_rapid)
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::contour::ContourType;
310
311 fn make_contour(id: usize, cx: f64, cy: f64, ct: ContourType) -> CutContour {
312 CutContour {
313 id,
314 geometry_id: format!("part{}", id),
315 instance: 0,
316 contour_type: ct,
317 vertices: vec![
318 (cx - 5.0, cy - 5.0),
319 (cx + 5.0, cy - 5.0),
320 (cx + 5.0, cy + 5.0),
321 (cx - 5.0, cy + 5.0),
322 ],
323 perimeter: 40.0,
324 centroid: (cx, cy),
325 }
326 }
327
328 #[test]
329 fn test_single_contour() {
330 let contours = vec![make_contour(0, 50.0, 50.0, ContourType::Exterior)];
331 let dag = CuttingDag::build(&contours);
332 let config = CuttingConfig::default();
333
334 let result = optimize_sequence(&contours, &dag, &config);
335 assert_eq!(result.order.len(), 1);
336 assert_eq!(result.order[0], 0);
337 }
338
339 #[test]
340 fn test_nn_selects_nearest() {
341 let contours = vec![
343 make_contour(0, 100.0, 0.0, ContourType::Exterior),
344 make_contour(1, 20.0, 0.0, ContourType::Exterior),
345 make_contour(2, 60.0, 0.0, ContourType::Exterior),
346 ];
347 let dag = CuttingDag::build(&contours);
348 let config = CuttingConfig::default();
349
350 let result = optimize_sequence(&contours, &dag, &config);
351 assert_eq!(result.order, vec![1, 2, 0]);
353 }
354
355 #[test]
356 fn test_precedence_respected() {
357 let contours = vec![
359 CutContour {
360 id: 0,
361 geometry_id: "part1".to_string(),
362 instance: 0,
363 contour_type: ContourType::Exterior,
364 vertices: vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)],
365 perimeter: 80.0,
366 centroid: (10.0, 10.0),
367 },
368 CutContour {
369 id: 1,
370 geometry_id: "part1".to_string(),
371 instance: 0,
372 contour_type: ContourType::Interior,
373 vertices: vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)],
374 perimeter: 40.0,
375 centroid: (10.0, 10.0),
376 },
377 ];
378 let dag = CuttingDag::build(&contours);
379 let config = CuttingConfig::default();
380
381 let result = optimize_sequence(&contours, &dag, &config);
382 let pos_interior = result
384 .order
385 .iter()
386 .position(|&id| id == 1)
387 .expect("interior should be in order");
388 let pos_exterior = result
389 .order
390 .iter()
391 .position(|&id| id == 0)
392 .expect("exterior should be in order");
393 assert!(pos_interior < pos_exterior);
394 }
395
396 #[test]
397 fn test_empty_contours() {
398 let contours: Vec<CutContour> = Vec::new();
399 let dag = CuttingDag::build(&contours);
400 let config = CuttingConfig::default();
401
402 let result = optimize_sequence(&contours, &dag, &config);
403 assert!(result.order.is_empty());
404 assert_eq!(result.total_rapid_distance, 0.0);
405 }
406
407 #[test]
408 fn test_nn_better_than_reverse() {
409 let contours: Vec<CutContour> = (0..5)
411 .map(|i| make_contour(i, 20.0 * i as f64 + 10.0, 10.0, ContourType::Exterior))
412 .collect();
413 let dag = CuttingDag::build(&contours);
414 let config = CuttingConfig::default();
415
416 let result = optimize_sequence(&contours, &dag, &config);
417
418 let reverse_order: Vec<usize> = (0..5).rev().collect();
420 let index = build_contour_index(&contours);
421 let (_, reverse_rapid) = compute_pierce_selections(&reverse_order, &index, &config);
422
423 assert!(
424 result.total_rapid_distance <= reverse_rapid + 1e-6,
425 "NN rapid {} should be <= reverse rapid {}",
426 result.total_rapid_distance,
427 reverse_rapid
428 );
429 }
430
431 #[test]
432 fn test_adjacency_bonus_prefers_neighbor() {
433 let contours = vec![
437 make_contour(0, 10.0, 10.0, ContourType::Exterior),
438 make_contour(1, 80.0, 10.0, ContourType::Exterior),
439 make_contour(2, 90.0, 10.0, ContourType::Exterior),
440 ];
441 let dag = CuttingDag::build(&contours);
442 let config = CuttingConfig::default();
443
444 let common_edges = CommonEdgeResult {
446 common_edges: vec![crate::common_edge::CommonEdge {
447 contour_a: 0,
448 edge_a: 0,
449 contour_b: 2,
450 edge_b: 0,
451 overlap_length: 10.0,
452 midpoint: (50.0, 10.0),
453 }],
454 total_common_length: 10.0,
455 };
456
457 let result_with =
458 optimize_sequence_with_adjacency(&contours, &dag, &config, Some(&common_edges));
459 let result_without = optimize_sequence(&contours, &dag, &config);
460
461 assert_eq!(result_with.order.len(), 3);
463 assert_eq!(result_without.order.len(), 3);
464
465 if result_with.order[0] == 0 {
468 assert_eq!(
469 result_with.order[1], 2,
470 "Adjacent contour 2 should follow contour 0"
471 );
472 }
473 }
474
475 #[test]
476 fn test_2opt_respects_time_limit() {
477 let contours: Vec<CutContour> = (0..1500)
487 .map(|i| {
488 let cx = (i % 40) as f64 * 20.0;
489 let cy = (i / 40) as f64 * 20.0;
490 make_contour(i, cx, cy, ContourType::Exterior)
491 })
492 .collect();
493 let dag = CuttingDag::build(&contours);
494 let config = CuttingConfig::default()
495 .with_max_2opt_iterations(1000)
496 .with_time_limit_ms(100);
497
498 let start = std::time::Instant::now();
499 let result = optimize_sequence(&contours, &dag, &config);
500 let elapsed = start.elapsed();
501
502 assert_eq!(result.order.len(), 1500);
504 assert!(
507 elapsed.as_secs() < 5,
508 "2-opt must respect time_limit_ms; took {elapsed:?}"
509 );
510 }
511
512 #[test]
513 fn test_zero_time_limit_is_unlimited() {
514 let contours: Vec<CutContour> = (0..5)
517 .map(|i| make_contour(i, 20.0 * i as f64 + 10.0, 10.0, ContourType::Exterior))
518 .collect();
519 let dag = CuttingDag::build(&contours);
520 let config = CuttingConfig::default().with_time_limit_ms(0);
521
522 let result = optimize_sequence(&contours, &dag, &config);
523 assert_eq!(result.order.len(), 5);
524 }
525
526 #[test]
527 fn test_adjacency_with_no_common_edges() {
528 let contours = vec![
530 make_contour(0, 10.0, 10.0, ContourType::Exterior),
531 make_contour(1, 30.0, 10.0, ContourType::Exterior),
532 ];
533 let dag = CuttingDag::build(&contours);
534 let config = CuttingConfig::default();
535
536 let result_with = optimize_sequence_with_adjacency(&contours, &dag, &config, None);
537 let result_without = optimize_sequence(&contours, &dag, &config);
538
539 assert_eq!(result_with.order, result_without.order);
540 }
541}