1use crate::boundary::Boundary2D;
21use crate::clamp_placement_to_boundary;
22use crate::geometry::Geometry2D;
23use crate::nfp::{
24 compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
25 verify_no_overlap_mirrored, Nfp, PlacedGeometry,
26};
27use std::sync::atomic::{AtomicBool, Ordering};
28use std::sync::Arc;
29use u_nesting_core::alns::{
30 AlnsConfig, AlnsProblem, AlnsResult, AlnsRunner, AlnsSolution, DestroyOperatorId,
31 DestroyResult, RepairOperatorId, RepairResult,
32};
33use u_nesting_core::geometry::{Boundary, Geometry};
34use u_nesting_core::solver::Config;
35use u_nesting_core::timing::Timer;
36use u_nesting_core::{Placement, SolveResult};
37
38use crate::placement_utils::{expand_nfp, shrink_ifp, InstanceInfo};
39use rand::prelude::*;
40
41#[derive(Debug, Clone)]
43pub struct PlacedItem {
44 pub instance_idx: usize,
46 pub x: f64,
48 pub y: f64,
50 pub rotation: f64,
52 pub mirrored: bool,
54 pub score: f64,
56}
57
58#[derive(Debug, Clone)]
60pub struct AlnsNestingSolution {
61 pub placed: Vec<PlacedItem>,
63 pub unplaced: Vec<usize>,
65 pub total_instances: usize,
67 pub placed_area: f64,
69 pub boundary_area: f64,
71 pub max_y: f64,
73}
74
75impl AlnsNestingSolution {
76 pub fn new(total_instances: usize, boundary_area: f64) -> Self {
78 Self {
79 placed: Vec::new(),
80 unplaced: (0..total_instances).collect(),
81 total_instances,
82 placed_area: 0.0,
83 boundary_area,
84 max_y: 0.0,
85 }
86 }
87}
88
89impl AlnsSolution for AlnsNestingSolution {
90 fn fitness(&self) -> f64 {
91 let unplaced_penalty = self.unplaced.len() as f64 * 1000.0;
93 let utilization_penalty = if self.placed_area > 0.0 {
94 1.0 - (self.placed_area / self.boundary_area)
95 } else {
96 1.0
97 };
98 let height_penalty = self.max_y / 1000.0;
99
100 unplaced_penalty + utilization_penalty + height_penalty
101 }
102
103 fn placed_count(&self) -> usize {
104 self.placed.len()
105 }
106
107 fn total_count(&self) -> usize {
108 self.total_instances
109 }
110}
111
112pub struct AlnsNestingProblem {
114 geometries: Vec<Geometry2D>,
116 boundary: Boundary2D,
118 config: Config,
120 instances: Vec<InstanceInfo>,
122 rotation_angles: Vec<Vec<f64>>,
124 geometry_areas: Vec<f64>,
126 cancelled: Arc<AtomicBool>,
128 start_time: Timer,
130 time_limit_ms: u64,
132}
133
134impl AlnsNestingProblem {
135 pub fn new(
137 geometries: Vec<Geometry2D>,
138 boundary: Boundary2D,
139 config: Config,
140 cancelled: Arc<AtomicBool>,
141 time_limit_ms: u64,
142 ) -> Self {
143 let mut instances = Vec::new();
144 let mut rotation_angles = Vec::new();
145 let mut geometry_areas = Vec::new();
146
147 for (geom_idx, geom) in geometries.iter().enumerate() {
148 let angles = geom.rotations();
149 let angles = if angles.is_empty() { vec![0.0] } else { angles };
150 rotation_angles.push(angles);
151
152 let area = geom.measure();
153 geometry_areas.push(area);
154
155 for instance_num in 0..geom.quantity() {
156 instances.push(InstanceInfo {
157 geometry_idx: geom_idx,
158 instance_num,
159 });
160 }
161 }
162
163 Self {
164 geometries,
165 boundary,
166 config,
167 instances,
168 rotation_angles,
169 geometry_areas,
170 cancelled,
171 start_time: Timer::now(),
172 time_limit_ms,
173 }
174 }
175
176 fn is_timed_out(&self) -> bool {
178 if self.time_limit_ms == 0 {
179 return false;
180 }
181 self.start_time.elapsed_ms() >= self.time_limit_ms
182 }
183
184 pub fn num_instances(&self) -> usize {
186 self.instances.len()
187 }
188
189 fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
191 let (min, max) = self.boundary.aabb();
192 vec![
193 (min[0] + margin, min[1] + margin),
194 (max[0] - margin, min[1] + margin),
195 (max[0] - margin, max[1] - margin),
196 (min[0] + margin, max[1] - margin),
197 ]
198 }
199
200 fn compute_sample_step(&self) -> f64 {
202 let (min, max) = self.boundary.aabb();
203 let width = max[0] - min[0];
204 (width / 100.0).max(1.0)
205 }
206
207 fn try_place_item(
209 &self,
210 instance_idx: usize,
211 placed_geometries: &[PlacedGeometry],
212 boundary_polygon: &[(f64, f64)],
213 sample_step: f64,
214 ) -> Option<PlacedItem> {
215 let info = &self.instances[instance_idx];
216 let geom = &self.geometries[info.geometry_idx];
217 let angles = &self.rotation_angles[info.geometry_idx];
218 let mirror_candidates: &[bool] = if geom.allow_flip() {
221 &[false, true]
222 } else {
223 &[false]
224 };
225
226 let mut best_placement: Option<PlacedItem> = None;
227 let mut best_y = f64::MAX;
228
229 for &rotation in angles {
230 for &mirror in mirror_candidates {
231 let ifp = match compute_ifp_with_margin_and_mirror(
232 boundary_polygon,
233 geom,
234 rotation,
235 0.0,
236 mirror,
237 ) {
238 Ok(ifp) => ifp,
239 Err(_) => continue,
240 };
241
242 if ifp.is_empty() {
243 continue;
244 }
245
246 let spacing = self.config.spacing;
247 let mut nfps: Vec<Nfp> = Vec::new();
248
249 for pg in placed_geometries {
250 let placed_exterior = pg.translated_exterior();
253 let placed_geom = Geometry2D::new(format!("_placed_{}", pg.geometry.id()))
254 .with_polygon(placed_exterior);
255
256 if let Ok(nfp) =
257 compute_nfp_mirrored(&placed_geom, geom, rotation, false, mirror)
258 {
259 let expanded = expand_nfp(&nfp, spacing);
260 nfps.push(expanded);
261 }
262 }
263
264 let ifp_shrunk = shrink_ifp(&ifp, spacing);
265 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
266
267 if let Some((x, y)) =
270 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
271 {
272 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
276 let boundary_aabb = self.boundary.aabb();
277
278 if let Some((clamped_x, clamped_y)) =
279 clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
280 {
281 let was_clamped =
284 (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
285 if was_clamped {
286 if !verify_no_overlap_mirrored(
288 geom,
289 (clamped_x, clamped_y),
290 rotation,
291 mirror,
292 placed_geometries,
293 ) {
294 continue; }
296 }
297
298 if clamped_y < best_y {
299 best_y = clamped_y;
300 best_placement = Some(PlacedItem {
301 instance_idx,
302 x: clamped_x,
303 y: clamped_y,
304 rotation,
305 mirrored: mirror,
306 score: clamped_y,
307 });
308 }
309 }
310 }
311 }
312 }
313
314 best_placement
315 }
316
317 fn place_items_blf(&self, items: &[usize], solution: &mut AlnsNestingSolution) {
319 let margin = self.config.margin;
320 let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
321 let sample_step = self.compute_sample_step();
322
323 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
324 for item in &solution.placed {
325 let info = &self.instances[item.instance_idx];
326 let geom = &self.geometries[info.geometry_idx];
327 placed_geometries.push(PlacedGeometry {
328 geometry: geom.clone(),
329 position: (item.x, item.y),
330 rotation: item.rotation,
331 mirrored: item.mirrored,
332 });
333 }
334
335 let mut sorted_items = items.to_vec();
337 sorted_items.sort_by(|&a, &b| {
338 let area_a = self.geometry_areas[self.instances[a].geometry_idx];
339 let area_b = self.geometry_areas[self.instances[b].geometry_idx];
340 area_b
341 .partial_cmp(&area_a)
342 .unwrap_or(std::cmp::Ordering::Equal)
343 });
344
345 for &instance_idx in &sorted_items {
346 if self.cancelled.load(Ordering::Relaxed) || self.is_timed_out() {
348 break;
349 }
350
351 if let Some(placement) = self.try_place_item(
352 instance_idx,
353 &placed_geometries,
354 &boundary_polygon,
355 sample_step,
356 ) {
357 let info = &self.instances[instance_idx];
358 let area = self.geometry_areas[info.geometry_idx];
359
360 solution.placed_area += area;
361 solution.max_y = solution.max_y.max(placement.y);
362
363 let geom = &self.geometries[info.geometry_idx];
364 placed_geometries.push(PlacedGeometry {
365 geometry: geom.clone(),
366 position: (placement.x, placement.y),
367 rotation: placement.rotation,
368 mirrored: placement.mirrored,
369 });
370
371 solution.placed.push(placement);
372 solution.unplaced.retain(|&idx| idx != instance_idx);
373 }
374 }
375 }
376
377 fn remove_item(&self, solution: &mut AlnsNestingSolution, instance_idx: usize) {
379 if let Some(pos) = solution
380 .placed
381 .iter()
382 .position(|p| p.instance_idx == instance_idx)
383 {
384 let item = solution.placed.remove(pos);
385 let info = &self.instances[item.instance_idx];
386 solution.placed_area -= self.geometry_areas[info.geometry_idx];
387 solution.unplaced.push(item.instance_idx);
388 }
389
390 solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
392 }
393}
394
395impl AlnsProblem for AlnsNestingProblem {
396 type Solution = AlnsNestingSolution;
397
398 fn create_initial_solution(&mut self) -> AlnsNestingSolution {
399 let boundary_area = self.boundary.measure();
400 let mut solution = AlnsNestingSolution::new(self.instances.len(), boundary_area);
401
402 let all_items: Vec<usize> = (0..self.instances.len()).collect();
403 self.place_items_blf(&all_items, &mut solution);
404
405 solution
406 }
407
408 fn clone_solution(&self, solution: &AlnsNestingSolution) -> AlnsNestingSolution {
409 solution.clone()
410 }
411
412 fn destroy_operators(&self) -> Vec<DestroyOperatorId> {
413 vec![
414 DestroyOperatorId::Random,
415 DestroyOperatorId::Worst,
416 DestroyOperatorId::Related,
417 DestroyOperatorId::Shaw,
418 ]
419 }
420
421 fn repair_operators(&self) -> Vec<RepairOperatorId> {
422 vec![
423 RepairOperatorId::Greedy,
424 RepairOperatorId::BottomLeftFill,
425 RepairOperatorId::Random,
426 ]
427 }
428
429 fn destroy(
430 &mut self,
431 solution: &mut AlnsNestingSolution,
432 operator: DestroyOperatorId,
433 degree: f64,
434 rng: &mut rand::rngs::StdRng,
435 ) -> DestroyResult {
436 let num_to_remove = ((solution.placed.len() as f64 * degree).ceil() as usize).max(1);
437 let mut removed_indices = Vec::new();
438
439 if solution.placed.is_empty() {
440 return DestroyResult {
441 removed_indices,
442 operator,
443 };
444 }
445
446 match operator {
447 DestroyOperatorId::Random => {
448 let mut indices: Vec<usize> =
450 solution.placed.iter().map(|p| p.instance_idx).collect();
451 indices.shuffle(rng);
452
453 for &idx in indices.iter().take(num_to_remove) {
454 removed_indices.push(idx);
455 }
456 }
457 DestroyOperatorId::Worst => {
458 let mut items_with_score: Vec<(usize, f64)> = solution
460 .placed
461 .iter()
462 .map(|p| (p.instance_idx, p.score))
463 .collect();
464
465 items_with_score
466 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
467
468 for (idx, _) in items_with_score.iter().take(num_to_remove) {
469 removed_indices.push(*idx);
470 }
471 }
472 DestroyOperatorId::Related | DestroyOperatorId::Shaw => {
473 let seed_idx = rng.random_range(0..solution.placed.len());
475 let seed = &solution.placed[seed_idx];
476 let seed_x = seed.x;
477 let seed_y = seed.y;
478
479 let mut items_with_distance: Vec<(usize, f64)> = solution
480 .placed
481 .iter()
482 .map(|item| {
483 let dx = item.x - seed_x;
484 let dy = item.y - seed_y;
485 (item.instance_idx, (dx * dx + dy * dy).sqrt())
486 })
487 .collect();
488
489 items_with_distance
490 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
491
492 for (idx, _) in items_with_distance.iter().take(num_to_remove) {
493 removed_indices.push(*idx);
494 }
495 }
496 DestroyOperatorId::Custom(_) => {
497 let mut indices: Vec<usize> =
499 solution.placed.iter().map(|p| p.instance_idx).collect();
500 indices.shuffle(rng);
501
502 for &idx in indices.iter().take(num_to_remove) {
503 removed_indices.push(idx);
504 }
505 }
506 }
507
508 for &idx in &removed_indices {
510 self.remove_item(solution, idx);
511 }
512
513 DestroyResult {
514 removed_indices,
515 operator,
516 }
517 }
518
519 fn repair(
520 &mut self,
521 solution: &mut AlnsNestingSolution,
522 _destroyed: &DestroyResult,
523 operator: RepairOperatorId,
524 ) -> RepairResult {
525 let items_to_place = solution.unplaced.clone();
526 let initial_placed = solution.placed.len();
527
528 match operator {
529 RepairOperatorId::Greedy | RepairOperatorId::BottomLeftFill => {
530 self.place_items_blf(&items_to_place, solution);
532 }
533 RepairOperatorId::Regret => {
534 self.place_items_blf(&items_to_place, solution);
536 }
537 RepairOperatorId::Random => {
538 let mut shuffled = items_to_place.clone();
540 use rand::SeedableRng;
541 let mut rng = rand::rngs::StdRng::from_os_rng();
542 shuffled.shuffle(&mut rng);
543 self.place_items_blf(&shuffled, solution);
544 }
545 RepairOperatorId::Custom(_) => {
546 self.place_items_blf(&items_to_place, solution);
547 }
548 }
549
550 RepairResult {
551 placed_count: solution.placed.len() - initial_placed,
552 unplaced_count: solution.unplaced.len(),
553 operator,
554 }
555 }
556
557 fn relatedness(&self, solution: &AlnsNestingSolution, i: usize, j: usize) -> f64 {
558 let item_i = solution.placed.iter().find(|p| p.instance_idx == i);
560 let item_j = solution.placed.iter().find(|p| p.instance_idx == j);
561
562 match (item_i, item_j) {
563 (Some(a), Some(b)) => {
564 let dx = a.x - b.x;
565 let dy = a.y - b.y;
566 1.0 / (1.0 + (dx * dx + dy * dy).sqrt())
567 }
568 _ => 0.0,
569 }
570 }
571}
572
573pub fn run_alns_nesting(
575 geometries: &[Geometry2D],
576 boundary: &Boundary2D,
577 config: &Config,
578 alns_config: &AlnsConfig,
579 cancelled: Arc<AtomicBool>,
580) -> SolveResult<f64> {
581 let mut problem = AlnsNestingProblem::new(
582 geometries.to_vec(),
583 boundary.clone(),
584 config.clone(),
585 cancelled,
586 alns_config.time_limit_ms,
587 );
588
589 let runner = AlnsRunner::new(alns_config.clone());
590 let alns_result: AlnsResult<AlnsNestingSolution> = runner.run(&mut problem, |_progress| {
591 });
593
594 let mut result = SolveResult::new();
595
596 for item in &alns_result.best_solution.placed {
597 let info = &problem.instances[item.instance_idx];
598 let geom = &problem.geometries[info.geometry_idx];
599
600 result.placements.push(
601 Placement::new_2d(
602 geom.id().to_string(),
603 info.instance_num,
604 item.x,
605 item.y,
606 item.rotation,
607 )
608 .with_mirrored(item.mirrored),
609 );
610 }
611
612 result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
613 result.utilization =
614 alns_result.best_solution.placed_area / alns_result.best_solution.boundary_area;
615 result.computation_time_ms = alns_result.elapsed_ms;
616 result.iterations = Some(alns_result.iterations as u64);
617 result.best_fitness = Some(alns_result.best_fitness);
618 result.strategy = Some("ALNS".to_string());
619
620 result
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 fn create_test_geometries() -> Vec<Geometry2D> {
628 vec![
629 Geometry2D::rectangle("rect1", 50.0, 30.0).with_quantity(3),
630 Geometry2D::rectangle("rect2", 40.0, 40.0).with_quantity(2),
631 Geometry2D::rectangle("rect3", 60.0, 20.0).with_quantity(2),
632 ]
633 }
634
635 fn create_test_boundary() -> Boundary2D {
636 Boundary2D::rectangle(300.0, 200.0)
637 }
638
639 #[test]
640 fn test_alns_nesting_problem_creation() {
641 let geometries = create_test_geometries();
642 let boundary = create_test_boundary();
643 let config = Config::default();
644 let cancelled = Arc::new(AtomicBool::new(false));
645
646 let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
647
648 assert_eq!(problem.num_instances(), 7);
649 }
650
651 #[test]
652 fn test_alns_nesting_initial_solution() {
653 let geometries = create_test_geometries();
654 let boundary = create_test_boundary();
655 let config = Config::default();
656 let cancelled = Arc::new(AtomicBool::new(false));
657
658 let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
659 let solution = problem.create_initial_solution();
660
661 assert!(!solution.placed.is_empty());
662 assert!(solution.placed_area > 0.0);
663 }
664
665 #[test]
666 fn test_alns_nesting_solution_fitness() {
667 let solution = AlnsNestingSolution {
668 placed: vec![
669 PlacedItem {
670 instance_idx: 0,
671 x: 10.0,
672 y: 10.0,
673 rotation: 0.0,
674 mirrored: false,
675 score: 10.0,
676 },
677 PlacedItem {
678 instance_idx: 1,
679 x: 60.0,
680 y: 10.0,
681 rotation: 0.0,
682 mirrored: false,
683 score: 10.0,
684 },
685 ],
686 unplaced: vec![2],
687 total_instances: 3,
688 placed_area: 3000.0,
689 boundary_area: 60000.0,
690 max_y: 50.0,
691 };
692
693 let fitness = solution.fitness();
694 assert!(fitness > 0.0);
695 assert!(fitness >= 1000.0); }
697
698 #[test]
699 fn test_alns_nesting_destroy_random() {
700 use rand::SeedableRng;
701
702 let geometries = create_test_geometries();
703 let boundary = create_test_boundary();
704 let config = Config::default();
705 let cancelled = Arc::new(AtomicBool::new(false));
706
707 let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
708 let mut solution = problem.create_initial_solution();
709
710 let initial_placed = solution.placed.len();
711 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
712
713 let result = problem.destroy(&mut solution, DestroyOperatorId::Random, 0.3, &mut rng);
714
715 assert!(!result.removed_indices.is_empty());
716 assert_eq!(result.operator, DestroyOperatorId::Random);
717 assert!(solution.placed.len() < initial_placed);
718 }
719
720 #[test]
721 fn test_alns_nesting_destroy_worst() {
722 use rand::SeedableRng;
723
724 let geometries = create_test_geometries();
725 let boundary = create_test_boundary();
726 let config = Config::default();
727 let cancelled = Arc::new(AtomicBool::new(false));
728
729 let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
730 let mut solution = problem.create_initial_solution();
731
732 let initial_placed = solution.placed.len();
733 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
734
735 let result = problem.destroy(&mut solution, DestroyOperatorId::Worst, 0.3, &mut rng);
736
737 assert!(!result.removed_indices.is_empty());
738 assert_eq!(result.operator, DestroyOperatorId::Worst);
739 assert!(solution.placed.len() < initial_placed);
740 }
741
742 #[test]
743 fn test_alns_nesting_repair() {
744 use rand::SeedableRng;
745
746 let geometries = create_test_geometries();
747 let boundary = create_test_boundary();
748 let config = Config::default();
749 let cancelled = Arc::new(AtomicBool::new(false));
750
751 let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
752 let mut solution = problem.create_initial_solution();
753
754 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
755
756 let destroy_result =
757 problem.destroy(&mut solution, DestroyOperatorId::Random, 0.5, &mut rng);
758 let after_destroy_placed = solution.placed.len();
759
760 let repair_result =
761 problem.repair(&mut solution, &destroy_result, RepairOperatorId::Greedy);
762
763 assert!(repair_result.placed_count > 0 || after_destroy_placed == solution.placed.len());
764 assert_eq!(repair_result.operator, RepairOperatorId::Greedy);
765 }
766
767 #[test]
768 fn test_run_alns_nesting() {
769 let geometries = create_test_geometries();
770 let boundary = create_test_boundary();
771 let config = Config::default();
772 let alns_config = AlnsConfig::new()
773 .with_max_iterations(50)
774 .with_time_limit_ms(5000);
775 let cancelled = Arc::new(AtomicBool::new(false));
776
777 let result = run_alns_nesting(&geometries, &boundary, &config, &alns_config, cancelled);
778
779 assert!(!result.placements.is_empty());
780 assert!(result.utilization > 0.0);
781 }
782
783 #[test]
784 fn test_alns_nesting_full_cycle() {
785 let geometries = create_test_geometries();
786 let boundary = create_test_boundary();
787 let config = Config::default();
788 let cancelled = Arc::new(AtomicBool::new(false));
789
790 let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
791
792 let alns_config = AlnsConfig::new().with_max_iterations(10).with_seed(42);
793
794 let runner = AlnsRunner::new(alns_config);
795 let result: AlnsResult<AlnsNestingSolution> = runner.run(&mut problem, |progress| {
796 assert!(progress.iteration <= 10);
797 });
798
799 assert!(result.iterations <= 10);
800 assert!(!result.best_solution.placed.is_empty());
801 }
802
803 #[test]
804 fn test_alns_destroy_operators() {
805 let geometries = create_test_geometries();
806 let boundary = create_test_boundary();
807 let config = Config::default();
808 let cancelled = Arc::new(AtomicBool::new(false));
809
810 let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
811 let operators = problem.destroy_operators();
812
813 assert!(operators.contains(&DestroyOperatorId::Random));
814 assert!(operators.contains(&DestroyOperatorId::Worst));
815 assert!(operators.contains(&DestroyOperatorId::Related));
816 assert!(operators.contains(&DestroyOperatorId::Shaw));
817 }
818
819 #[test]
820 fn test_alns_repair_operators() {
821 let geometries = create_test_geometries();
822 let boundary = create_test_boundary();
823 let config = Config::default();
824 let cancelled = Arc::new(AtomicBool::new(false));
825
826 let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
827 let operators = problem.repair_operators();
828
829 assert!(operators.contains(&RepairOperatorId::Greedy));
830 assert!(operators.contains(&RepairOperatorId::BottomLeftFill));
831 assert!(operators.contains(&RepairOperatorId::Random));
832 }
833
834 fn chiral_l(id: &str) -> Geometry2D {
837 Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
838 }
839
840 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
841 for i in 0..a.len() {
842 let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
843 for j in 0..b.len() {
844 let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
845 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
846 return true;
847 }
848 }
849 }
850 false
851 }
852
853 #[test]
858 fn test_alns_try_place_item_mirror_no_overlap() {
859 let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
860 let boundary = Boundary2D::rectangle(65.0, 45.0);
861 let config = Config::default().with_spacing(1.0);
862 let problem = AlnsNestingProblem::new(
863 geometries.clone(),
864 boundary,
865 config,
866 Arc::new(AtomicBool::new(false)),
867 60000,
868 );
869
870 let margin = problem.config.margin;
871 let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
872 let sample_step = problem.compute_sample_step();
873
874 let placement0 = problem
875 .try_place_item(0, &[], &boundary_polygon, sample_step)
876 .expect("first piece must fit");
877
878 let placed_geometries = vec![PlacedGeometry {
879 geometry: geometries[0].clone(),
880 position: (placement0.x, placement0.y),
881 rotation: placement0.rotation,
882 mirrored: placement0.mirrored,
883 }];
884 let placement1 = problem
885 .try_place_item(1, &placed_geometries, &boundary_polygon, sample_step)
886 .expect("second piece must fit avoiding the first");
887
888 let poly0 = PlacedGeometry {
889 geometry: geometries[0].clone(),
890 position: (placement0.x, placement0.y),
891 rotation: placement0.rotation,
892 mirrored: placement0.mirrored,
893 }
894 .translated_exterior();
895 let poly1 = PlacedGeometry {
896 geometry: geometries[0].clone(),
897 position: (placement1.x, placement1.y),
898 rotation: placement1.rotation,
899 mirrored: placement1.mirrored,
900 }
901 .translated_exterior();
902 assert!(
903 !polygons_overlap(&poly0, &poly1),
904 "instance 0 (mirrored={}) and instance 1 (mirrored={}) must not overlap",
905 placement0.mirrored,
906 placement1.mirrored
907 );
908 }
909
910 #[test]
911 fn test_alns_try_place_item_mirror_ignored_without_allow_flip() {
912 let geometries = vec![chiral_l("L").with_quantity(1)];
913 let boundary = Boundary2D::rectangle(65.0, 45.0);
914 let problem = AlnsNestingProblem::new(
915 geometries,
916 boundary,
917 Config::default(),
918 Arc::new(AtomicBool::new(false)),
919 60000,
920 );
921
922 let margin = problem.config.margin;
923 let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
924 let sample_step = problem.compute_sample_step();
925
926 let placement = problem
927 .try_place_item(0, &[], &boundary_polygon, sample_step)
928 .expect("piece must fit");
929 assert!(
930 !placement.mirrored,
931 "allow_flip=false must suppress mirroring"
932 );
933 }
934}