1use crate::boundary::Boundary2D;
19use crate::clamp_placement_to_boundary;
20use crate::geometry::Geometry2D;
21use crate::nfp::{
22 compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
23 verify_no_overlap_mirrored, Nfp, PlacedGeometry,
24};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::Arc;
27use u_nesting_core::gdrr::{
28 GdrrConfig, GdrrProblem, GdrrResult, GdrrRunner, GdrrSolution, RecreateResult, RecreateType,
29 RuinResult, RuinType, RuinedItem,
30};
31use u_nesting_core::geometry::{Boundary, Geometry};
32use u_nesting_core::solver::Config;
33use u_nesting_core::timing::Timer;
34use u_nesting_core::{Placement, SolveResult};
35
36use crate::placement_utils::{expand_nfp, shrink_ifp, InstanceInfo};
37use rand::prelude::*;
38
39#[derive(Debug, Clone)]
41pub struct PlacedItem {
42 pub instance_idx: usize,
44 pub x: f64,
46 pub y: f64,
48 pub rotation: f64,
50 pub mirrored: bool,
52 pub score: f64,
54}
55
56#[derive(Debug, Clone)]
58pub struct GdrrNestingSolution {
59 pub placed: Vec<PlacedItem>,
61 pub unplaced: Vec<usize>,
63 pub total_instances: usize,
65 pub placed_area: f64,
67 pub boundary_area: f64,
69 pub max_y: f64,
71}
72
73impl GdrrNestingSolution {
74 pub fn new(total_instances: usize, boundary_area: f64) -> Self {
76 Self {
77 placed: Vec::new(),
78 unplaced: (0..total_instances).collect(),
79 total_instances,
80 placed_area: 0.0,
81 boundary_area,
82 max_y: 0.0,
83 }
84 }
85}
86
87impl GdrrSolution for GdrrNestingSolution {
88 fn fitness(&self) -> f64 {
89 let unplaced_penalty = self.unplaced.len() as f64 * 1000.0;
94 let utilization_penalty = if self.placed_area > 0.0 {
95 1.0 - (self.placed_area / self.boundary_area)
96 } else {
97 1.0
98 };
99 let height_penalty = self.max_y / 1000.0; unplaced_penalty + utilization_penalty + height_penalty
102 }
103
104 fn placed_count(&self) -> usize {
105 self.placed.len()
106 }
107
108 fn total_count(&self) -> usize {
109 self.total_instances
110 }
111
112 fn utilization(&self) -> f64 {
113 if self.boundary_area > 0.0 {
114 self.placed_area / self.boundary_area
115 } else {
116 0.0
117 }
118 }
119
120 fn fits_goal(&self, goal: f64) -> bool {
121 self.fitness() <= goal
123 }
124}
125
126pub struct GdrrNestingProblem {
128 geometries: Vec<Geometry2D>,
130 boundary: Boundary2D,
132 config: Config,
134 instances: Vec<InstanceInfo>,
136 rotation_angles: Vec<Vec<f64>>,
138 geometry_areas: Vec<f64>,
140 cancelled: Arc<AtomicBool>,
142 start_time: Timer,
144 time_limit_ms: u64,
146}
147
148impl GdrrNestingProblem {
149 pub fn new(
151 geometries: Vec<Geometry2D>,
152 boundary: Boundary2D,
153 config: Config,
154 cancelled: Arc<AtomicBool>,
155 time_limit_ms: u64,
156 ) -> Self {
157 let mut instances = Vec::new();
159 let mut rotation_angles = Vec::new();
160 let mut geometry_areas = Vec::new();
161
162 for (geom_idx, geom) in geometries.iter().enumerate() {
163 let angles = geom.rotations();
165 let angles = if angles.is_empty() { vec![0.0] } else { angles };
166 rotation_angles.push(angles);
167
168 let area = geom.measure();
170 geometry_areas.push(area);
171
172 for instance_num in 0..geom.quantity() {
174 instances.push(InstanceInfo {
175 geometry_idx: geom_idx,
176 instance_num,
177 });
178 }
179 }
180
181 Self {
182 geometries,
183 boundary,
184 config,
185 instances,
186 rotation_angles,
187 geometry_areas,
188 cancelled,
189 start_time: Timer::now(),
190 time_limit_ms,
191 }
192 }
193
194 fn is_timed_out(&self) -> bool {
196 if self.time_limit_ms == 0 {
197 return false;
198 }
199 self.start_time.elapsed_ms() >= self.time_limit_ms
200 }
201
202 pub fn num_instances(&self) -> usize {
204 self.instances.len()
205 }
206
207 fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
209 let (min, max) = self.boundary.aabb();
210 vec![
211 (min[0] + margin, min[1] + margin),
212 (max[0] - margin, min[1] + margin),
213 (max[0] - margin, max[1] - margin),
214 (min[0] + margin, max[1] - margin),
215 ]
216 }
217
218 fn compute_sample_step(&self) -> f64 {
220 let (min, max) = self.boundary.aabb();
221 let width = max[0] - min[0];
222 (width / 100.0).max(1.0)
223 }
224
225 fn try_place_item(
227 &self,
228 instance_idx: usize,
229 placed_geometries: &[PlacedGeometry],
230 boundary_polygon: &[(f64, f64)],
231 sample_step: f64,
232 ) -> Option<PlacedItem> {
233 let info = &self.instances[instance_idx];
234 let geom = &self.geometries[info.geometry_idx];
235 let angles = &self.rotation_angles[info.geometry_idx];
236 let mirror_candidates: &[bool] = if geom.allow_flip() {
239 &[false, true]
240 } else {
241 &[false]
242 };
243
244 let mut best_placement: Option<PlacedItem> = None;
245 let mut best_y = f64::MAX;
246
247 for &rotation in angles {
248 for &mirror in mirror_candidates {
249 let ifp = match compute_ifp_with_margin_and_mirror(
251 boundary_polygon,
252 geom,
253 rotation,
254 0.0,
255 mirror,
256 ) {
257 Ok(ifp) => ifp,
258 Err(_) => continue,
259 };
260
261 if ifp.is_empty() {
262 continue;
263 }
264
265 let spacing = self.config.spacing;
267 let mut nfps: Vec<Nfp> = Vec::new();
268
269 for pg in placed_geometries {
270 let placed_exterior = pg.translated_exterior();
273 let placed_geom = Geometry2D::new(format!("_placed_{}", pg.geometry.id()))
274 .with_polygon(placed_exterior);
275
276 if let Ok(nfp) =
277 compute_nfp_mirrored(&placed_geom, geom, rotation, false, mirror)
278 {
279 let expanded = expand_nfp(&nfp, spacing);
280 nfps.push(expanded);
281 }
282 }
283
284 let ifp_shrunk = shrink_ifp(&ifp, spacing);
286
287 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
291 if let Some((x, y)) =
292 find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
293 {
294 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
298 let boundary_aabb = self.boundary.aabb();
299
300 if let Some((clamped_x, clamped_y)) =
301 clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
302 {
303 let was_clamped =
306 (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
307 if was_clamped {
308 if !verify_no_overlap_mirrored(
310 geom,
311 (clamped_x, clamped_y),
312 rotation,
313 mirror,
314 placed_geometries,
315 ) {
316 continue; }
318 }
319
320 if clamped_y < best_y {
321 best_y = clamped_y;
322 best_placement = Some(PlacedItem {
323 instance_idx,
324 x: clamped_x,
325 y: clamped_y,
326 rotation,
327 mirrored: mirror,
328 score: clamped_y, });
330 }
331 }
332 }
333 }
334 }
335
336 best_placement
337 }
338
339 fn place_items_blf(&self, items: &[usize], solution: &mut GdrrNestingSolution) {
341 let margin = self.config.margin;
342 let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
343 let sample_step = self.compute_sample_step();
344
345 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
347 for item in &solution.placed {
348 let info = &self.instances[item.instance_idx];
349 let geom = &self.geometries[info.geometry_idx];
350 placed_geometries.push(PlacedGeometry {
351 geometry: geom.clone(),
352 position: (item.x, item.y),
353 rotation: item.rotation,
354 mirrored: item.mirrored,
355 });
356 }
357
358 let mut sorted_items = items.to_vec();
360 sorted_items.sort_by(|&a, &b| {
361 let area_a = self.geometry_areas[self.instances[a].geometry_idx];
362 let area_b = self.geometry_areas[self.instances[b].geometry_idx];
363 area_b
364 .partial_cmp(&area_a)
365 .unwrap_or(std::cmp::Ordering::Equal)
366 });
367
368 for &instance_idx in &sorted_items {
369 if self.cancelled.load(Ordering::Relaxed) || self.is_timed_out() {
371 break;
372 }
373
374 if let Some(placement) = self.try_place_item(
375 instance_idx,
376 &placed_geometries,
377 &boundary_polygon,
378 sample_step,
379 ) {
380 let info = &self.instances[instance_idx];
382 let area = self.geometry_areas[info.geometry_idx];
383
384 solution.placed_area += area;
385 solution.max_y = solution.max_y.max(placement.y);
386
387 let geom = &self.geometries[info.geometry_idx];
389 placed_geometries.push(PlacedGeometry {
390 geometry: geom.clone(),
391 position: (placement.x, placement.y),
392 rotation: placement.rotation,
393 mirrored: placement.mirrored,
394 });
395
396 solution.placed.push(placement);
397 solution.unplaced.retain(|&idx| idx != instance_idx);
398 }
399 }
400 }
401}
402
403impl GdrrProblem for GdrrNestingProblem {
404 type Solution = GdrrNestingSolution;
405
406 fn create_initial_solution(&mut self) -> GdrrNestingSolution {
407 let boundary_area = self.boundary.measure();
408 let mut solution = GdrrNestingSolution::new(self.instances.len(), boundary_area);
409
410 let all_items: Vec<usize> = (0..self.instances.len()).collect();
412 self.place_items_blf(&all_items, &mut solution);
413
414 solution
415 }
416
417 fn clone_solution(&self, solution: &GdrrNestingSolution) -> GdrrNestingSolution {
418 solution.clone()
419 }
420
421 fn ruin_random(
422 &mut self,
423 solution: &mut GdrrNestingSolution,
424 ratio: f64,
425 rng: &mut rand::rngs::StdRng,
426 ) -> RuinResult {
427 let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
428 let mut removed_items = Vec::new();
429
430 if solution.placed.is_empty() {
431 return RuinResult {
432 removed_items,
433 ruin_type: RuinType::Random,
434 };
435 }
436
437 let mut indices: Vec<usize> = (0..solution.placed.len()).collect();
439 indices.shuffle(rng);
440
441 for &idx in indices.iter().take(num_to_remove) {
442 let item = &solution.placed[idx];
443 let info = &self.instances[item.instance_idx];
444
445 removed_items.push(RuinedItem {
446 index: item.instance_idx,
447 geometry_id: self.geometries[info.geometry_idx].id().to_string(),
448 position: vec![item.x, item.y],
449 rotation: item.rotation,
450 score: item.score,
451 });
452 }
453
454 let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
456
457 for idx in &removed_instance_indices {
458 if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
459 let item = solution.placed.remove(pos);
460 let info = &self.instances[item.instance_idx];
461 solution.placed_area -= self.geometry_areas[info.geometry_idx];
462 solution.unplaced.push(item.instance_idx);
463 }
464 }
465
466 solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
468
469 RuinResult {
470 removed_items,
471 ruin_type: RuinType::Random,
472 }
473 }
474
475 fn ruin_cluster(
476 &mut self,
477 solution: &mut GdrrNestingSolution,
478 ratio: f64,
479 rng: &mut rand::rngs::StdRng,
480 ) -> RuinResult {
481 let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
482 let mut removed_items = Vec::new();
483
484 if solution.placed.is_empty() {
485 return RuinResult {
486 removed_items,
487 ruin_type: RuinType::Cluster,
488 };
489 }
490
491 let seed_idx = rng.random_range(0..solution.placed.len());
493 let seed = &solution.placed[seed_idx];
494 let seed_x = seed.x;
495 let seed_y = seed.y;
496
497 let mut items_with_distance: Vec<(usize, f64)> = solution
499 .placed
500 .iter()
501 .enumerate()
502 .map(|(idx, item)| {
503 let dx = item.x - seed_x;
504 let dy = item.y - seed_y;
505 (idx, (dx * dx + dy * dy).sqrt())
506 })
507 .collect();
508
509 items_with_distance
510 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
511
512 for (idx, _) in items_with_distance.iter().take(num_to_remove) {
514 let item = &solution.placed[*idx];
515 let info = &self.instances[item.instance_idx];
516
517 removed_items.push(RuinedItem {
518 index: item.instance_idx,
519 geometry_id: self.geometries[info.geometry_idx].id().to_string(),
520 position: vec![item.x, item.y],
521 rotation: item.rotation,
522 score: item.score,
523 });
524 }
525
526 let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
528
529 for idx in &removed_instance_indices {
530 if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
531 let item = solution.placed.remove(pos);
532 let info = &self.instances[item.instance_idx];
533 solution.placed_area -= self.geometry_areas[info.geometry_idx];
534 solution.unplaced.push(item.instance_idx);
535 }
536 }
537
538 solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
540
541 RuinResult {
542 removed_items,
543 ruin_type: RuinType::Cluster,
544 }
545 }
546
547 fn ruin_worst(
548 &mut self,
549 solution: &mut GdrrNestingSolution,
550 ratio: f64,
551 _rng: &mut rand::rngs::StdRng,
552 ) -> RuinResult {
553 let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
554 let mut removed_items = Vec::new();
555
556 if solution.placed.is_empty() {
557 return RuinResult {
558 removed_items,
559 ruin_type: RuinType::Worst,
560 };
561 }
562
563 let mut items_with_score: Vec<(usize, f64)> = solution
565 .placed
566 .iter()
567 .enumerate()
568 .map(|(idx, item)| (idx, item.score))
569 .collect();
570
571 items_with_score.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
572
573 for (idx, _) in items_with_score.iter().take(num_to_remove) {
575 let item = &solution.placed[*idx];
576 let info = &self.instances[item.instance_idx];
577
578 removed_items.push(RuinedItem {
579 index: item.instance_idx,
580 geometry_id: self.geometries[info.geometry_idx].id().to_string(),
581 position: vec![item.x, item.y],
582 rotation: item.rotation,
583 score: item.score,
584 });
585 }
586
587 let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
589
590 for idx in &removed_instance_indices {
591 if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
592 let item = solution.placed.remove(pos);
593 let info = &self.instances[item.instance_idx];
594 solution.placed_area -= self.geometry_areas[info.geometry_idx];
595 solution.unplaced.push(item.instance_idx);
596 }
597 }
598
599 solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
601
602 RuinResult {
603 removed_items,
604 ruin_type: RuinType::Worst,
605 }
606 }
607
608 fn recreate_best_fit(
609 &mut self,
610 solution: &mut GdrrNestingSolution,
611 _ruined: &RuinResult,
612 ) -> RecreateResult {
613 let items_to_place = solution.unplaced.clone();
614 let initial_placed = solution.placed.len();
615
616 self.place_items_blf(&items_to_place, solution);
617
618 RecreateResult {
619 placed_count: solution.placed.len() - initial_placed,
620 unplaced_count: solution.unplaced.len(),
621 recreate_type: RecreateType::BestFit,
622 }
623 }
624
625 fn recreate_blf(
626 &mut self,
627 solution: &mut GdrrNestingSolution,
628 _ruined: &RuinResult,
629 ) -> RecreateResult {
630 let items_to_place = solution.unplaced.clone();
631 let initial_placed = solution.placed.len();
632
633 self.place_items_blf(&items_to_place, solution);
634
635 RecreateResult {
636 placed_count: solution.placed.len() - initial_placed,
637 unplaced_count: solution.unplaced.len(),
638 recreate_type: RecreateType::BottomLeftFill,
639 }
640 }
641
642 fn recreate_nfp(
643 &mut self,
644 solution: &mut GdrrNestingSolution,
645 _ruined: &RuinResult,
646 ) -> RecreateResult {
647 let items_to_place = solution.unplaced.clone();
649 let initial_placed = solution.placed.len();
650
651 self.place_items_blf(&items_to_place, solution);
652
653 RecreateResult {
654 placed_count: solution.placed.len() - initial_placed,
655 unplaced_count: solution.unplaced.len(),
656 recreate_type: RecreateType::NfpGuided,
657 }
658 }
659
660 fn placement_score(&self, solution: &GdrrNestingSolution, item_index: usize) -> f64 {
661 solution
662 .placed
663 .iter()
664 .find(|p| p.instance_idx == item_index)
665 .map(|p| p.score)
666 .unwrap_or(f64::MAX)
667 }
668
669 fn get_neighbors(
670 &self,
671 solution: &GdrrNestingSolution,
672 item_index: usize,
673 radius: f64,
674 ) -> Vec<usize> {
675 let item = match solution
676 .placed
677 .iter()
678 .find(|p| p.instance_idx == item_index)
679 {
680 Some(i) => i,
681 None => return vec![],
682 };
683
684 solution
685 .placed
686 .iter()
687 .filter(|p| {
688 if p.instance_idx == item_index {
689 return false;
690 }
691 let dx = p.x - item.x;
692 let dy = p.y - item.y;
693 (dx * dx + dy * dy).sqrt() <= radius
694 })
695 .map(|p| p.instance_idx)
696 .collect()
697 }
698}
699
700pub fn run_gdrr_nesting(
702 geometries: &[Geometry2D],
703 boundary: &Boundary2D,
704 config: &Config,
705 gdrr_config: &GdrrConfig,
706 cancelled: Arc<AtomicBool>,
707) -> SolveResult<f64> {
708 let mut problem = GdrrNestingProblem::new(
709 geometries.to_vec(),
710 boundary.clone(),
711 config.clone(),
712 cancelled,
713 gdrr_config.time_limit_ms,
714 );
715
716 let runner = GdrrRunner::new(gdrr_config.clone());
717 let gdrr_result: GdrrResult<GdrrNestingSolution> = runner.run(&mut problem, |_progress| {
718 });
720
721 let mut result = SolveResult::new();
723
724 for item in &gdrr_result.best_solution.placed {
725 let info = &problem.instances[item.instance_idx];
726 let geom = &problem.geometries[info.geometry_idx];
727
728 result.placements.push(
729 Placement::new_2d(
730 geom.id().to_string(),
731 info.instance_num,
732 item.x,
733 item.y,
734 item.rotation,
735 )
736 .with_mirrored(item.mirrored),
737 );
738 }
739
740 result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
741 result.utilization = gdrr_result.best_solution.utilization();
742 result.computation_time_ms = gdrr_result.elapsed_ms;
743 result.iterations = Some(gdrr_result.iterations as u64);
744 result.best_fitness = Some(gdrr_result.best_fitness);
745 result.strategy = Some("GDRR".to_string());
746
747 result
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753
754 fn create_test_geometries() -> Vec<Geometry2D> {
755 vec![
756 Geometry2D::rectangle("rect1", 50.0, 30.0).with_quantity(3),
757 Geometry2D::rectangle("rect2", 40.0, 40.0).with_quantity(2),
758 Geometry2D::rectangle("rect3", 60.0, 20.0).with_quantity(2),
759 ]
760 }
761
762 fn create_test_boundary() -> Boundary2D {
763 Boundary2D::rectangle(300.0, 200.0)
764 }
765
766 #[test]
767 fn test_gdrr_nesting_problem_creation() {
768 let geometries = create_test_geometries();
769 let boundary = create_test_boundary();
770 let config = Config::default();
771 let cancelled = Arc::new(AtomicBool::new(false));
772
773 let problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
774
775 assert_eq!(problem.num_instances(), 7); }
777
778 #[test]
779 fn test_gdrr_nesting_initial_solution() {
780 let geometries = create_test_geometries();
781 let boundary = create_test_boundary();
782 let config = Config::default();
783 let cancelled = Arc::new(AtomicBool::new(false));
784
785 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
786 let solution = problem.create_initial_solution();
787
788 assert!(!solution.placed.is_empty());
789 assert!(solution.placed_area > 0.0);
790 }
791
792 #[test]
793 fn test_gdrr_nesting_solution_fitness() {
794 let solution = GdrrNestingSolution {
795 placed: vec![
796 PlacedItem {
797 instance_idx: 0,
798 x: 10.0,
799 y: 10.0,
800 rotation: 0.0,
801 mirrored: false,
802 score: 10.0,
803 },
804 PlacedItem {
805 instance_idx: 1,
806 x: 60.0,
807 y: 10.0,
808 rotation: 0.0,
809 mirrored: false,
810 score: 10.0,
811 },
812 ],
813 unplaced: vec![2],
814 total_instances: 3,
815 placed_area: 3000.0,
816 boundary_area: 60000.0,
817 max_y: 50.0,
818 };
819
820 let fitness = solution.fitness();
821 assert!(fitness > 0.0);
822 assert!(fitness >= 1000.0);
824 }
825
826 #[test]
827 fn test_gdrr_nesting_ruin_random() {
828 use rand::SeedableRng;
829
830 let geometries = create_test_geometries();
831 let boundary = create_test_boundary();
832 let config = Config::default();
833 let cancelled = Arc::new(AtomicBool::new(false));
834
835 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
836 let mut solution = problem.create_initial_solution();
837
838 let initial_placed = solution.placed.len();
839 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
840
841 let result = problem.ruin_random(&mut solution, 0.3, &mut rng);
842
843 assert!(!result.removed_items.is_empty());
844 assert_eq!(result.ruin_type, RuinType::Random);
845 assert!(solution.placed.len() < initial_placed);
846 }
847
848 #[test]
849 fn test_gdrr_nesting_ruin_cluster() {
850 use rand::SeedableRng;
851
852 let geometries = create_test_geometries();
853 let boundary = create_test_boundary();
854 let config = Config::default();
855 let cancelled = Arc::new(AtomicBool::new(false));
856
857 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
858 let mut solution = problem.create_initial_solution();
859
860 let initial_placed = solution.placed.len();
861 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
862
863 let result = problem.ruin_cluster(&mut solution, 0.3, &mut rng);
864
865 assert!(!result.removed_items.is_empty());
866 assert_eq!(result.ruin_type, RuinType::Cluster);
867 assert!(solution.placed.len() < initial_placed);
868 }
869
870 #[test]
871 fn test_gdrr_nesting_ruin_worst() {
872 use rand::SeedableRng;
873
874 let geometries = create_test_geometries();
875 let boundary = create_test_boundary();
876 let config = Config::default();
877 let cancelled = Arc::new(AtomicBool::new(false));
878
879 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
880 let mut solution = problem.create_initial_solution();
881
882 let initial_placed = solution.placed.len();
883 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
884
885 let result = problem.ruin_worst(&mut solution, 0.3, &mut rng);
886
887 assert!(!result.removed_items.is_empty());
888 assert_eq!(result.ruin_type, RuinType::Worst);
889 assert!(solution.placed.len() < initial_placed);
890 }
891
892 #[test]
893 fn test_gdrr_nesting_recreate() {
894 use rand::SeedableRng;
895
896 let geometries = create_test_geometries();
897 let boundary = create_test_boundary();
898 let config = Config::default();
899 let cancelled = Arc::new(AtomicBool::new(false));
900
901 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
902 let mut solution = problem.create_initial_solution();
903
904 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
905
906 let ruin_result = problem.ruin_random(&mut solution, 0.5, &mut rng);
908 let after_ruin_placed = solution.placed.len();
909
910 let recreate_result = problem.recreate_best_fit(&mut solution, &ruin_result);
912
913 assert!(recreate_result.placed_count > 0 || after_ruin_placed == solution.placed.len());
914 assert_eq!(recreate_result.recreate_type, RecreateType::BestFit);
915 }
916
917 #[test]
918 fn test_run_gdrr_nesting() {
919 let geometries = create_test_geometries();
920 let boundary = create_test_boundary();
921 let config = Config::default();
922 let gdrr_config = GdrrConfig::new()
923 .with_max_iterations(50)
924 .with_time_limit_ms(5000);
925 let cancelled = Arc::new(AtomicBool::new(false));
926
927 let result = run_gdrr_nesting(&geometries, &boundary, &config, &gdrr_config, cancelled);
928
929 assert!(!result.placements.is_empty());
930 assert!(result.utilization > 0.0);
931 }
932
933 #[test]
934 fn test_gdrr_nesting_full_cycle() {
935 let geometries = create_test_geometries();
936 let boundary = create_test_boundary();
937 let config = Config::default();
938 let cancelled = Arc::new(AtomicBool::new(false));
939
940 let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
941
942 let gdrr_config = GdrrConfig::new().with_max_iterations(10).with_seed(42);
943
944 let runner = GdrrRunner::new(gdrr_config);
945 let result: GdrrResult<GdrrNestingSolution> = runner.run(&mut problem, |progress| {
946 assert!(progress.iteration <= 10);
947 });
948
949 assert!(result.iterations <= 10);
950 assert!(!result.best_solution.placed.is_empty());
951 }
952
953 fn chiral_l(id: &str) -> Geometry2D {
956 Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
957 }
958
959 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
960 for i in 0..a.len() {
961 let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
962 for j in 0..b.len() {
963 let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
964 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
965 return true;
966 }
967 }
968 }
969 false
970 }
971
972 #[test]
978 fn test_gdrr_try_place_item_mirror_no_overlap() {
979 let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
980 let boundary = Boundary2D::rectangle(65.0, 45.0);
981 let config = Config::default().with_spacing(1.0);
982 let problem = GdrrNestingProblem::new(
983 geometries.clone(),
984 boundary,
985 config,
986 Arc::new(AtomicBool::new(false)),
987 60000,
988 );
989
990 let margin = problem.config.margin;
991 let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
992 let sample_step = problem.compute_sample_step();
993
994 let placement0 = problem
996 .try_place_item(0, &[], &boundary_polygon, sample_step)
997 .expect("first piece must fit");
998
999 let placed_geometries = vec![PlacedGeometry {
1001 geometry: geometries[0].clone(),
1002 position: (placement0.x, placement0.y),
1003 rotation: placement0.rotation,
1004 mirrored: placement0.mirrored,
1005 }];
1006 let placement1 = problem
1007 .try_place_item(1, &placed_geometries, &boundary_polygon, sample_step)
1008 .expect("second piece must fit avoiding the first");
1009
1010 let poly0 = PlacedGeometry {
1019 geometry: geometries[0].clone(),
1020 position: (placement0.x, placement0.y),
1021 rotation: placement0.rotation,
1022 mirrored: placement0.mirrored,
1023 }
1024 .translated_exterior();
1025 let poly1 = PlacedGeometry {
1026 geometry: geometries[0].clone(),
1027 position: (placement1.x, placement1.y),
1028 rotation: placement1.rotation,
1029 mirrored: placement1.mirrored,
1030 }
1031 .translated_exterior();
1032 assert!(
1033 !polygons_overlap(&poly0, &poly1),
1034 "instance 0 (mirrored={}) and instance 1 (mirrored={}) must not overlap",
1035 placement0.mirrored,
1036 placement1.mirrored
1037 );
1038 }
1039
1040 #[test]
1041 fn test_gdrr_try_place_item_mirror_ignored_without_allow_flip() {
1042 let geometries = vec![chiral_l("L").with_quantity(1)];
1043 let boundary = Boundary2D::rectangle(65.0, 45.0);
1044 let problem = GdrrNestingProblem::new(
1045 geometries,
1046 boundary,
1047 Config::default(),
1048 Arc::new(AtomicBool::new(false)),
1049 60000,
1050 );
1051
1052 let margin = problem.config.margin;
1053 let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
1054 let sample_step = problem.compute_sample_step();
1055
1056 let placement = problem
1057 .try_place_item(0, &[], &boundary_polygon, sample_step)
1058 .expect("piece must fit");
1059 assert!(
1060 !placement.mirrored,
1061 "allow_flip=false must suppress mirroring"
1062 );
1063 }
1064}