1use std::collections::{HashMap, HashSet};
18
19use crate::common_edge::CommonEdgeResult;
20use crate::config::CuttingConfig;
21use crate::contour::CutContour;
22use crate::cost::{closest_point_on_polygon, point_distance};
23use crate::hierarchy::CuttingDag;
24use crate::pierce::{select_pierce, PierceSelection};
25
26#[derive(Debug, Clone)]
28pub struct SequenceResult {
29 pub order: Vec<usize>,
31 pub pierce_selections: Vec<PierceSelection>,
33 pub total_rapid_distance: f64,
35}
36
37pub fn optimize_sequence(
39 contours: &[CutContour],
40 dag: &CuttingDag,
41 config: &CuttingConfig,
42) -> SequenceResult {
43 optimize_sequence_with_adjacency(contours, dag, config, None)
44}
45
46pub fn optimize_sequence_with_adjacency(
53 contours: &[CutContour],
54 dag: &CuttingDag,
55 config: &CuttingConfig,
56 common_edges: Option<&CommonEdgeResult>,
57) -> SequenceResult {
58 if contours.is_empty() {
59 return SequenceResult {
60 order: Vec::new(),
61 pierce_selections: Vec::new(),
62 total_rapid_distance: 0.0,
63 };
64 }
65
66 let adjacency = build_adjacency_map(common_edges);
68
69 let mut order = nearest_neighbor_with_adjacency(contours, dag, config, &adjacency);
71
72 if config.max_2opt_iterations > 0 {
74 improve_2opt(&mut order, contours, dag, config);
75 }
76
77 let (pierce_selections, total_rapid) = compute_pierce_selections(&order, contours, config);
79
80 SequenceResult {
81 order,
82 pierce_selections,
83 total_rapid_distance: total_rapid,
84 }
85}
86
87fn build_adjacency_map(
90 common_edges: Option<&CommonEdgeResult>,
91) -> HashMap<usize, Vec<(usize, f64)>> {
92 let mut adjacency: HashMap<usize, Vec<(usize, f64)>> = HashMap::new();
93
94 if let Some(result) = common_edges {
95 for edge in &result.common_edges {
96 adjacency
97 .entry(edge.contour_a)
98 .or_default()
99 .push((edge.contour_b, edge.overlap_length));
100 adjacency
101 .entry(edge.contour_b)
102 .or_default()
103 .push((edge.contour_a, edge.overlap_length));
104 }
105 }
106
107 adjacency
108}
109
110fn nearest_neighbor_with_adjacency(
116 contours: &[CutContour],
117 dag: &CuttingDag,
118 config: &CuttingConfig,
119 adjacency: &HashMap<usize, Vec<(usize, f64)>>,
120) -> Vec<usize> {
121 let n = contours.len();
122 let mut visited: HashSet<usize> = HashSet::with_capacity(n);
123 let mut order = Vec::with_capacity(n);
124 let mut current_pos = config.home_position;
125 let mut last_id: Option<usize> = None;
126
127 const ADJACENCY_DISCOUNT: f64 = 0.5;
130
131 for _ in 0..n {
132 let mut best_idx = None;
134 let mut best_score = f64::MAX;
135
136 for contour in contours.iter() {
137 if visited.contains(&contour.id) {
138 continue;
139 }
140
141 let predecessors = dag.predecessors(contour.id);
143 let ready = predecessors.iter().all(|pred_id| visited.contains(pred_id));
144
145 if !ready {
146 continue;
147 }
148
149 let dist = closest_point_on_polygon(&contour.vertices, current_pos)
151 .map(|(pt, _, _)| point_distance(current_pos, pt))
152 .unwrap_or(f64::MAX);
153
154 let mut score = dist;
156 if let Some(last) = last_id {
157 if let Some(neighbors) = adjacency.get(&last) {
158 if neighbors.iter().any(|(adj_id, _)| *adj_id == contour.id) {
159 score *= ADJACENCY_DISCOUNT;
160 }
161 }
162 }
163
164 if score < best_score {
165 best_score = score;
166 best_idx = Some(contour.id);
167 }
168 }
169
170 if let Some(id) = best_idx {
171 visited.insert(id);
172 order.push(id);
173 last_id = Some(id);
174
175 if let Some(contour) = contours.iter().find(|c| c.id == id) {
177 let pierce = select_pierce(contour, current_pos, config);
178 current_pos = pierce.end_point;
179 }
180 }
181 }
182
183 order
184}
185
186fn improve_2opt(
191 order: &mut [usize],
192 contours: &[CutContour],
193 dag: &CuttingDag,
194 config: &CuttingConfig,
195) {
196 let n = order.len();
197 if n < 3 {
198 return;
199 }
200
201 let mut improved = true;
202 let mut iterations = 0;
203 let mut current_rapid = compute_pierce_selections(order, contours, config).1;
204
205 while improved && iterations < config.max_2opt_iterations {
206 improved = false;
207 iterations += 1;
208
209 for i in 0..n - 1 {
210 for j in (i + 2)..n {
211 order[i + 1..=j].reverse();
213
214 if dag.is_valid_sequence(order) {
216 let new_rapid = compute_pierce_selections(order, contours, config).1;
217
218 if new_rapid < current_rapid - 1e-10 {
219 current_rapid = new_rapid;
220 improved = true;
221 } else {
222 order[i + 1..=j].reverse(); }
224 } else {
225 order[i + 1..=j].reverse(); }
227 }
228 }
229 }
230}
231
232fn compute_pierce_selections(
234 order: &[usize],
235 contours: &[CutContour],
236 config: &CuttingConfig,
237) -> (Vec<PierceSelection>, f64) {
238 let mut selections = Vec::with_capacity(order.len());
239 let mut total_rapid = 0.0;
240 let mut current_pos = config.home_position;
241
242 for &contour_id in order {
243 let contour = match contours.iter().find(|c| c.id == contour_id) {
244 Some(c) => c,
245 None => continue,
246 };
247
248 let pierce = select_pierce(contour, current_pos, config);
249 let rapid = point_distance(current_pos, pierce.point);
250 total_rapid += rapid;
251 current_pos = pierce.end_point;
252 selections.push(pierce);
253 }
254
255 (selections, total_rapid)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::contour::ContourType;
262
263 fn make_contour(id: usize, cx: f64, cy: f64, ct: ContourType) -> CutContour {
264 CutContour {
265 id,
266 geometry_id: format!("part{}", id),
267 instance: 0,
268 contour_type: ct,
269 vertices: vec![
270 (cx - 5.0, cy - 5.0),
271 (cx + 5.0, cy - 5.0),
272 (cx + 5.0, cy + 5.0),
273 (cx - 5.0, cy + 5.0),
274 ],
275 perimeter: 40.0,
276 centroid: (cx, cy),
277 }
278 }
279
280 #[test]
281 fn test_single_contour() {
282 let contours = vec![make_contour(0, 50.0, 50.0, ContourType::Exterior)];
283 let dag = CuttingDag::build(&contours);
284 let config = CuttingConfig::default();
285
286 let result = optimize_sequence(&contours, &dag, &config);
287 assert_eq!(result.order.len(), 1);
288 assert_eq!(result.order[0], 0);
289 }
290
291 #[test]
292 fn test_nn_selects_nearest() {
293 let contours = vec![
295 make_contour(0, 100.0, 0.0, ContourType::Exterior),
296 make_contour(1, 20.0, 0.0, ContourType::Exterior),
297 make_contour(2, 60.0, 0.0, ContourType::Exterior),
298 ];
299 let dag = CuttingDag::build(&contours);
300 let config = CuttingConfig::default();
301
302 let result = optimize_sequence(&contours, &dag, &config);
303 assert_eq!(result.order, vec![1, 2, 0]);
305 }
306
307 #[test]
308 fn test_precedence_respected() {
309 let contours = vec![
311 CutContour {
312 id: 0,
313 geometry_id: "part1".to_string(),
314 instance: 0,
315 contour_type: ContourType::Exterior,
316 vertices: vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)],
317 perimeter: 80.0,
318 centroid: (10.0, 10.0),
319 },
320 CutContour {
321 id: 1,
322 geometry_id: "part1".to_string(),
323 instance: 0,
324 contour_type: ContourType::Interior,
325 vertices: vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)],
326 perimeter: 40.0,
327 centroid: (10.0, 10.0),
328 },
329 ];
330 let dag = CuttingDag::build(&contours);
331 let config = CuttingConfig::default();
332
333 let result = optimize_sequence(&contours, &dag, &config);
334 let pos_interior = result
336 .order
337 .iter()
338 .position(|&id| id == 1)
339 .expect("interior should be in order");
340 let pos_exterior = result
341 .order
342 .iter()
343 .position(|&id| id == 0)
344 .expect("exterior should be in order");
345 assert!(pos_interior < pos_exterior);
346 }
347
348 #[test]
349 fn test_empty_contours() {
350 let contours: Vec<CutContour> = Vec::new();
351 let dag = CuttingDag::build(&contours);
352 let config = CuttingConfig::default();
353
354 let result = optimize_sequence(&contours, &dag, &config);
355 assert!(result.order.is_empty());
356 assert_eq!(result.total_rapid_distance, 0.0);
357 }
358
359 #[test]
360 fn test_nn_better_than_reverse() {
361 let contours: Vec<CutContour> = (0..5)
363 .map(|i| make_contour(i, 20.0 * i as f64 + 10.0, 10.0, ContourType::Exterior))
364 .collect();
365 let dag = CuttingDag::build(&contours);
366 let config = CuttingConfig::default();
367
368 let result = optimize_sequence(&contours, &dag, &config);
369
370 let reverse_order: Vec<usize> = (0..5).rev().collect();
372 let (_, reverse_rapid) = compute_pierce_selections(&reverse_order, &contours, &config);
373
374 assert!(
375 result.total_rapid_distance <= reverse_rapid + 1e-6,
376 "NN rapid {} should be <= reverse rapid {}",
377 result.total_rapid_distance,
378 reverse_rapid
379 );
380 }
381
382 #[test]
383 fn test_adjacency_bonus_prefers_neighbor() {
384 let contours = vec![
388 make_contour(0, 10.0, 10.0, ContourType::Exterior),
389 make_contour(1, 80.0, 10.0, ContourType::Exterior),
390 make_contour(2, 90.0, 10.0, ContourType::Exterior),
391 ];
392 let dag = CuttingDag::build(&contours);
393 let config = CuttingConfig::default();
394
395 let common_edges = CommonEdgeResult {
397 common_edges: vec![crate::common_edge::CommonEdge {
398 contour_a: 0,
399 edge_a: 0,
400 contour_b: 2,
401 edge_b: 0,
402 overlap_length: 10.0,
403 midpoint: (50.0, 10.0),
404 }],
405 total_common_length: 10.0,
406 };
407
408 let result_with =
409 optimize_sequence_with_adjacency(&contours, &dag, &config, Some(&common_edges));
410 let result_without = optimize_sequence(&contours, &dag, &config);
411
412 assert_eq!(result_with.order.len(), 3);
414 assert_eq!(result_without.order.len(), 3);
415
416 if result_with.order[0] == 0 {
419 assert_eq!(
420 result_with.order[1], 2,
421 "Adjacent contour 2 should follow contour 0"
422 );
423 }
424 }
425
426 #[test]
427 fn test_adjacency_with_no_common_edges() {
428 let contours = vec![
430 make_contour(0, 10.0, 10.0, ContourType::Exterior),
431 make_contour(1, 30.0, 10.0, ContourType::Exterior),
432 ];
433 let dag = CuttingDag::build(&contours);
434 let config = CuttingConfig::default();
435
436 let result_with = optimize_sequence_with_adjacency(&contours, &dag, &config, None);
437 let result_without = optimize_sequence(&contours, &dag, &config);
438
439 assert_eq!(result_with.order, result_without.order);
440 }
441}