Skip to main content

u_nesting_d3/
packer.rs

1//! 3D bin packing solver.
2
3use crate::boundary::Boundary3D;
4use crate::brkga_packing::run_brkga_packing;
5use crate::extreme_point::run_ep_packing;
6use crate::ga_packing::run_ga_packing;
7use crate::geometry::Geometry3D;
8use crate::physics::{PhysicsConfig, PhysicsSimulator};
9use crate::sa_packing::run_sa_packing;
10use crate::stability::{PlacedBox, StabilityAnalyzer, StabilityConstraint, StabilityReport};
11use u_nesting_core::brkga::BrkgaConfig;
12use u_nesting_core::ga::GaConfig;
13use u_nesting_core::geom::nalgebra_types::{NaPoint3 as Point3, NaVector3 as Vector3};
14use u_nesting_core::geometry::{Boundary, Geometry};
15use u_nesting_core::sa::SaConfig;
16use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo, Solver, Strategy};
17use u_nesting_core::{Placement, Result, SolveResult};
18
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use u_nesting_core::timing::Timer;
22
23/// Best fit candidate for 3D placement.
24/// (orientation_idx, width, depth, height, place_x, place_y, place_z, new_row_depth, new_layer_height)
25type BestFit3D = Option<(usize, f64, f64, f64, f64, f64, f64, f64, f64)>;
26
27/// 3D bin packing solver.
28pub struct Packer3D {
29    config: Config,
30    cancelled: Arc<AtomicBool>,
31}
32
33impl Packer3D {
34    /// Creates a new packer with the given configuration.
35    pub fn new(config: Config) -> Self {
36        Self {
37            config,
38            cancelled: Arc::new(AtomicBool::new(false)),
39        }
40    }
41
42    /// Creates a packer with default configuration.
43    pub fn default_config() -> Self {
44        Self::new(Config::default())
45    }
46
47    /// Validates the stability of a packing result.
48    ///
49    /// Analyzes each placement to ensure boxes are properly supported.
50    pub fn validate_stability(
51        &self,
52        result: &SolveResult<f64>,
53        geometries: &[Geometry3D],
54        _boundary: &Boundary3D,
55        constraint: StabilityConstraint,
56    ) -> StabilityReport {
57        // Convert placements to PlacedBox format
58        let placed_boxes = self.placements_to_boxes(result, geometries);
59        let analyzer = StabilityAnalyzer::new(constraint);
60        analyzer.analyze(&placed_boxes, 0.0)
61    }
62
63    /// Validates stability using physics simulation.
64    ///
65    /// Runs a physics simulation to detect boxes that would fall or tip.
66    pub fn validate_stability_physics(
67        &self,
68        result: &SolveResult<f64>,
69        geometries: &[Geometry3D],
70        boundary: &Boundary3D,
71    ) -> StabilityReport {
72        let placed_boxes = self.placements_to_boxes(result, geometries);
73        let container = Vector3::new(boundary.width(), boundary.depth(), boundary.height());
74
75        let config = PhysicsConfig::default().with_max_time(2.0);
76        let simulator = PhysicsSimulator::new(config);
77        simulator.validate_stability(&placed_boxes, container, 0.0)
78    }
79
80    /// Converts placements to PlacedBox format for stability analysis.
81    fn placements_to_boxes(
82        &self,
83        result: &SolveResult<f64>,
84        geometries: &[Geometry3D],
85    ) -> Vec<PlacedBox> {
86        let geom_map: std::collections::HashMap<&str, &Geometry3D> =
87            geometries.iter().map(|g| (g.id().as_str(), g)).collect();
88
89        result
90            .placements
91            .iter()
92            .filter_map(|p| {
93                let geom = geom_map.get(p.geometry_id.as_str())?;
94                let ori_idx = p.rotation_index.unwrap_or(0);
95                let dims = geom.dimensions_for_orientation(ori_idx);
96
97                let mut placed = PlacedBox::new(
98                    p.geometry_id.clone(),
99                    p.instance,
100                    Point3::new(p.position[0], p.position[1], p.position[2]),
101                    dims,
102                );
103
104                if let Some(mass) = geom.mass() {
105                    placed = placed.with_mass(mass);
106                }
107
108                Some(placed)
109            })
110            .collect()
111    }
112
113    /// Simple layer-based packing algorithm.
114    fn layer_packing(
115        &self,
116        geometries: &[Geometry3D],
117        boundary: &Boundary3D,
118    ) -> Result<SolveResult<f64>> {
119        let start = Timer::now();
120        let mut result = SolveResult::new();
121        let mut placements = Vec::new();
122
123        let margin = self.config.margin;
124        let spacing = self.config.spacing;
125
126        let bound_max_x = boundary.width() - margin;
127        let bound_max_y = boundary.depth() - margin;
128        let bound_max_z = boundary.height() - margin;
129
130        // Simple layer-based placement
131        let mut current_x = margin;
132        let mut current_y = margin;
133        let mut current_z = margin;
134        let mut row_depth = 0.0_f64;
135        let mut layer_height = 0.0_f64;
136
137        let mut total_placed_volume = 0.0;
138        let mut total_placed_mass = 0.0;
139
140        for geom in geometries {
141            geom.validate()?;
142
143            for instance in 0..geom.quantity() {
144                if self.cancelled.load(Ordering::Relaxed) {
145                    result.computation_time_ms = start.elapsed_ms();
146                    return Ok(result);
147                }
148
149                // Check time limit (0 = unlimited)
150                if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
151                {
152                    result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
153                    result.utilization = total_placed_volume / boundary.measure();
154                    result.computation_time_ms = start.elapsed_ms();
155                    result.placements = placements;
156                    return Ok(result);
157                }
158
159                // Check mass constraint
160                if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
161                    if total_placed_mass + item_mass > max_mass {
162                        result.unplaced.push(geom.id().clone());
163                        continue;
164                    }
165                }
166
167                // Try all allowed orientations to find the best fit
168                let orientations = geom.allowed_orientations();
169                let mut best_fit: BestFit3D = None;
170                // (orientation_idx, width, depth, height, place_x, place_y, place_z, new_row_depth, new_layer_height)
171
172                for (ori_idx, _) in orientations.iter().enumerate() {
173                    let dims = geom.dimensions_for_orientation(ori_idx);
174                    let g_width = dims.x;
175                    let g_depth = dims.y;
176                    let g_height = dims.z;
177
178                    // Try current position first
179                    let mut try_x = current_x;
180                    let mut try_y = current_y;
181                    let mut try_z = current_z;
182                    let mut try_row_depth = row_depth;
183                    let mut try_layer_height = layer_height;
184
185                    // Check if fits in current row
186                    if try_x + g_width > bound_max_x {
187                        try_x = margin;
188                        try_y += row_depth + spacing;
189                        try_row_depth = 0.0;
190                    }
191
192                    // Check if fits in current layer
193                    if try_y + g_depth > bound_max_y {
194                        try_x = margin;
195                        try_y = margin;
196                        try_z += layer_height + spacing;
197                        try_row_depth = 0.0;
198                        try_layer_height = 0.0;
199                    }
200
201                    // Check if fits in container
202                    if try_z + g_height > bound_max_z {
203                        continue; // This orientation doesn't fit
204                    }
205
206                    // Score: prefer placements that use less vertical space (height)
207                    // and stay in current row (lower y advancement)
208                    let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
209
210                    let is_better = match &best_fit {
211                        None => true,
212                        Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
213                            let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
214                            score < best_score
215                        }
216                    };
217
218                    if is_better {
219                        best_fit = Some((
220                            ori_idx,
221                            g_width,
222                            g_depth,
223                            g_height,
224                            try_x,
225                            try_y,
226                            try_z,
227                            try_row_depth,
228                            try_layer_height,
229                        ));
230                    }
231                }
232
233                if let Some((
234                    ori_idx,
235                    g_width,
236                    g_depth,
237                    g_height,
238                    place_x,
239                    place_y,
240                    place_z,
241                    new_row_depth,
242                    new_layer_height,
243                )) = best_fit
244                {
245                    // Convert orientation index to rotation angles
246                    // For simplicity, we encode orientation in rotation_index
247                    let placement = Placement::new_3d(
248                        geom.id().clone(),
249                        instance,
250                        place_x,
251                        place_y,
252                        place_z,
253                        0.0, // Orientation is encoded via rotation_index
254                        0.0,
255                        0.0,
256                    )
257                    .with_rotation_index(ori_idx);
258
259                    placements.push(placement);
260                    total_placed_volume += geom.measure();
261                    if let Some(mass) = geom.mass() {
262                        total_placed_mass += mass;
263                    }
264
265                    // Update position for next item
266                    current_x = place_x + g_width + spacing;
267                    current_y = place_y;
268                    current_z = place_z;
269                    row_depth = new_row_depth.max(g_depth);
270                    layer_height = new_layer_height.max(g_height);
271                } else {
272                    result.unplaced.push(geom.id().clone());
273                }
274            }
275        }
276
277        result.placements = placements;
278        result.boundaries_used = 1;
279        result.utilization = total_placed_volume / boundary.measure();
280        result.computation_time_ms = start.elapsed_ms();
281
282        Ok(result)
283    }
284
285    /// Genetic Algorithm based packing optimization.
286    ///
287    /// Uses GA to optimize placement order and orientations, with layer-based
288    /// decoding for collision-free placements.
289    fn genetic_algorithm(
290        &self,
291        geometries: &[Geometry3D],
292        boundary: &Boundary3D,
293    ) -> Result<SolveResult<f64>> {
294        // Configure GA from solver config
295        let ga_config = GaConfig::default()
296            .with_population_size(self.config.population_size)
297            .with_max_generations(self.config.max_generations)
298            .with_crossover_rate(self.config.crossover_rate)
299            .with_mutation_rate(self.config.mutation_rate);
300
301        let result = run_ga_packing(
302            geometries,
303            boundary,
304            &self.config,
305            ga_config,
306            self.cancelled.clone(),
307        );
308
309        Ok(result)
310    }
311
312    /// BRKGA (Biased Random-Key Genetic Algorithm) based packing optimization.
313    ///
314    /// Uses random-key encoding and biased crossover for robust optimization.
315    fn brkga(&self, geometries: &[Geometry3D], boundary: &Boundary3D) -> Result<SolveResult<f64>> {
316        // Configure BRKGA with reasonable defaults
317        let brkga_config = BrkgaConfig::default()
318            .with_population_size(50)
319            .with_max_generations(100)
320            .with_elite_fraction(0.2)
321            .with_mutant_fraction(0.15)
322            .with_elite_bias(0.7);
323
324        let result = run_brkga_packing(
325            geometries,
326            boundary,
327            &self.config,
328            brkga_config,
329            self.cancelled.clone(),
330        );
331
332        Ok(result)
333    }
334
335    /// Simulated Annealing based packing optimization.
336    ///
337    /// Uses neighborhood operators to explore solution space with temperature-based
338    /// acceptance probability.
339    fn simulated_annealing(
340        &self,
341        geometries: &[Geometry3D],
342        boundary: &Boundary3D,
343    ) -> Result<SolveResult<f64>> {
344        // Configure SA with reasonable defaults
345        let sa_config = SaConfig::default()
346            .with_initial_temp(100.0)
347            .with_final_temp(0.1)
348            .with_cooling_rate(0.95)
349            .with_iterations_per_temp(50)
350            .with_max_iterations(10000);
351
352        let result = run_sa_packing(
353            geometries,
354            boundary,
355            &self.config,
356            sa_config,
357            self.cancelled.clone(),
358        );
359
360        Ok(result)
361    }
362
363    /// Extreme Point heuristic-based packing.
364    ///
365    /// Places boxes at extreme points (positions touching at least two surfaces).
366    /// More efficient than layer-based packing for many scenarios.
367    fn extreme_point(
368        &self,
369        geometries: &[Geometry3D],
370        boundary: &Boundary3D,
371    ) -> Result<SolveResult<f64>> {
372        let start = Timer::now();
373
374        let (ep_placements, utilization) = run_ep_packing(
375            geometries,
376            boundary,
377            self.config.margin,
378            self.config.spacing,
379            boundary.max_mass(),
380        );
381
382        // Convert EP placements to Placement structs
383        let mut placements = Vec::new();
384        for (id, instance, position, _orientation) in ep_placements {
385            let placement = Placement::new_3d(
386                id, instance, position.x, position.y, position.z, 0.0, // rotation_x
387                0.0, // rotation_y
388                0.0, // rotation_z (orientation handled internally)
389            );
390            placements.push(placement);
391        }
392
393        // Collect unplaced items
394        let mut placed_ids: std::collections::HashSet<(String, usize)> =
395            std::collections::HashSet::new();
396        for p in &placements {
397            placed_ids.insert((p.geometry_id.clone(), p.instance));
398        }
399
400        let mut unplaced = Vec::new();
401        for geom in geometries {
402            for instance in 0..geom.quantity() {
403                if !placed_ids.contains(&(geom.id().clone(), instance)) {
404                    unplaced.push(geom.id().clone());
405                }
406            }
407        }
408
409        let mut result = SolveResult::new();
410        result.placements = placements;
411        result.boundaries_used = 1;
412        result.utilization = utilization;
413        result.unplaced = unplaced;
414        result.computation_time_ms = start.elapsed_ms();
415        result.strategy = Some("ExtremePoint".to_string());
416
417        Ok(result)
418    }
419
420    /// Layer packing with progress callback.
421    fn layer_packing_with_progress(
422        &self,
423        geometries: &[Geometry3D],
424        boundary: &Boundary3D,
425        callback: &ProgressCallback,
426    ) -> Result<SolveResult<f64>> {
427        let start = Timer::now();
428        let mut result = SolveResult::new();
429        let mut placements = Vec::new();
430
431        let margin = self.config.margin;
432        let spacing = self.config.spacing;
433
434        let bound_max_x = boundary.width() - margin;
435        let bound_max_y = boundary.depth() - margin;
436        let bound_max_z = boundary.height() - margin;
437
438        let mut current_x = margin;
439        let mut current_y = margin;
440        let mut current_z = margin;
441        let mut row_depth = 0.0_f64;
442        let mut layer_height = 0.0_f64;
443
444        let mut total_placed_volume = 0.0;
445        let mut total_placed_mass = 0.0;
446
447        // Count total pieces for progress
448        let total_pieces: usize = geometries.iter().map(|g| g.quantity()).sum();
449        let mut placed_count = 0usize;
450
451        // Initial progress callback
452        callback(
453            ProgressInfo::new()
454                .with_phase("Layer Packing")
455                .with_items(0, total_pieces)
456                .with_elapsed(0),
457        );
458
459        for geom in geometries {
460            geom.validate()?;
461
462            for instance in 0..geom.quantity() {
463                if self.cancelled.load(Ordering::Relaxed) {
464                    result.computation_time_ms = start.elapsed_ms();
465                    callback(
466                        ProgressInfo::new()
467                            .with_phase("Cancelled")
468                            .with_items(placed_count, total_pieces)
469                            .with_elapsed(result.computation_time_ms)
470                            .finished(),
471                    );
472                    return Ok(result);
473                }
474
475                // Check time limit (0 = unlimited)
476                if self.config.time_limit_ms > 0 && start.elapsed_ms() >= self.config.time_limit_ms
477                {
478                    result.boundaries_used = if placements.is_empty() { 0 } else { 1 };
479                    result.utilization = total_placed_volume / boundary.measure();
480                    result.computation_time_ms = start.elapsed_ms();
481                    result.placements = placements;
482                    callback(
483                        ProgressInfo::new()
484                            .with_phase("Time Limit Reached")
485                            .with_items(placed_count, total_pieces)
486                            .with_elapsed(result.computation_time_ms)
487                            .finished(),
488                    );
489                    return Ok(result);
490                }
491
492                // Check mass constraint
493                if let (Some(max_mass), Some(item_mass)) = (boundary.max_mass(), geom.mass()) {
494                    if total_placed_mass + item_mass > max_mass {
495                        result.unplaced.push(geom.id().clone());
496                        continue;
497                    }
498                }
499
500                // Try all allowed orientations to find the best fit
501                let orientations = geom.allowed_orientations();
502                let mut best_fit: BestFit3D = None;
503
504                for (ori_idx, _) in orientations.iter().enumerate() {
505                    let dims = geom.dimensions_for_orientation(ori_idx);
506                    let g_width = dims.x;
507                    let g_depth = dims.y;
508                    let g_height = dims.z;
509
510                    let mut try_x = current_x;
511                    let mut try_y = current_y;
512                    let mut try_z = current_z;
513                    let mut try_row_depth = row_depth;
514                    let mut try_layer_height = layer_height;
515
516                    if try_x + g_width > bound_max_x {
517                        try_x = margin;
518                        try_y += row_depth + spacing;
519                        try_row_depth = 0.0;
520                    }
521
522                    if try_y + g_depth > bound_max_y {
523                        try_x = margin;
524                        try_y = margin;
525                        try_z += layer_height + spacing;
526                        try_row_depth = 0.0;
527                        try_layer_height = 0.0;
528                    }
529
530                    if try_z + g_height > bound_max_z {
531                        continue;
532                    }
533
534                    let score = try_z * 1000000.0 + try_y * 1000.0 + try_x + g_height * 0.1;
535
536                    let is_better = match &best_fit {
537                        None => true,
538                        Some((_, _, _, bg_height, bx, by, bz, _, _)) => {
539                            let best_score = bz * 1000000.0 + by * 1000.0 + bx + bg_height * 0.1;
540                            score < best_score
541                        }
542                    };
543
544                    if is_better {
545                        best_fit = Some((
546                            ori_idx,
547                            g_width,
548                            g_depth,
549                            g_height,
550                            try_x,
551                            try_y,
552                            try_z,
553                            try_row_depth,
554                            try_layer_height,
555                        ));
556                    }
557                }
558
559                if let Some((
560                    ori_idx,
561                    g_width,
562                    g_depth,
563                    g_height,
564                    place_x,
565                    place_y,
566                    place_z,
567                    new_row_depth,
568                    new_layer_height,
569                )) = best_fit
570                {
571                    let placement = Placement::new_3d(
572                        geom.id().clone(),
573                        instance,
574                        place_x,
575                        place_y,
576                        place_z,
577                        0.0,
578                        0.0,
579                        0.0,
580                    )
581                    .with_rotation_index(ori_idx);
582
583                    placements.push(placement);
584                    total_placed_volume += geom.measure();
585                    if let Some(mass) = geom.mass() {
586                        total_placed_mass += mass;
587                    }
588                    placed_count += 1;
589
590                    current_x = place_x + g_width + spacing;
591                    current_y = place_y;
592                    current_z = place_z;
593                    row_depth = new_row_depth.max(g_depth);
594                    layer_height = new_layer_height.max(g_height);
595
596                    // Progress callback every piece
597                    callback(
598                        ProgressInfo::new()
599                            .with_phase("Layer Packing")
600                            .with_items(placed_count, total_pieces)
601                            .with_utilization(total_placed_volume / boundary.measure())
602                            .with_elapsed(start.elapsed_ms()),
603                    );
604                } else {
605                    result.unplaced.push(geom.id().clone());
606                }
607            }
608        }
609
610        result.placements = placements;
611        result.boundaries_used = 1;
612        result.utilization = total_placed_volume / boundary.measure();
613        result.computation_time_ms = start.elapsed_ms();
614
615        // Final progress callback
616        callback(
617            ProgressInfo::new()
618                .with_phase("Complete")
619                .with_items(placed_count, total_pieces)
620                .with_utilization(result.utilization)
621                .with_elapsed(result.computation_time_ms)
622                .finished(),
623        );
624
625        Ok(result)
626    }
627}
628
629impl Solver for Packer3D {
630    type Geometry = Geometry3D;
631    type Boundary = Boundary3D;
632    type Scalar = f64;
633
634    fn solve(
635        &self,
636        geometries: &[Self::Geometry],
637        boundary: &Self::Boundary,
638    ) -> Result<SolveResult<f64>> {
639        boundary.validate()?;
640
641        // Reset cancellation flag
642        self.cancelled.store(false, Ordering::Relaxed);
643
644        let mut result = match self.config.strategy {
645            Strategy::BottomLeftFill => self.layer_packing(geometries, boundary),
646            Strategy::ExtremePoint => self.extreme_point(geometries, boundary),
647            Strategy::GeneticAlgorithm => self.genetic_algorithm(geometries, boundary),
648            Strategy::Brkga => self.brkga(geometries, boundary),
649            Strategy::SimulatedAnnealing => self.simulated_annealing(geometries, boundary),
650            _ => {
651                // Fall back to layer packing for unimplemented strategies
652                log::warn!(
653                    "Strategy {:?} not yet implemented, using layer packing",
654                    self.config.strategy
655                );
656                self.layer_packing(geometries, boundary)
657            }
658        }?;
659
660        // Remove duplicate entries from unplaced list
661        result.deduplicate_unplaced();
662        Ok(result)
663    }
664
665    fn solve_with_progress(
666        &self,
667        geometries: &[Self::Geometry],
668        boundary: &Self::Boundary,
669        callback: ProgressCallback,
670    ) -> Result<SolveResult<f64>> {
671        boundary.validate()?;
672
673        // Reset cancellation flag
674        self.cancelled.store(false, Ordering::Relaxed);
675
676        let mut result = match self.config.strategy {
677            Strategy::BottomLeftFill | Strategy::ExtremePoint => {
678                self.layer_packing_with_progress(geometries, boundary, &callback)?
679            }
680            // Other strategies fall back to basic progress reporting
681            _ => {
682                log::warn!(
683                    "Strategy {:?} progress not yet implemented, using layer packing",
684                    self.config.strategy
685                );
686                self.layer_packing_with_progress(geometries, boundary, &callback)?
687            }
688        };
689
690        // Remove duplicate entries from unplaced list
691        result.deduplicate_unplaced();
692        Ok(result)
693    }
694
695    fn cancel(&self) {
696        self.cancelled.store(true, Ordering::Relaxed);
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[test]
705    fn test_simple_packing() {
706        let geometries = vec![
707            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(3),
708            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
709        ];
710
711        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
712        let packer = Packer3D::default_config();
713
714        let result = packer.solve(&geometries, &boundary).unwrap();
715
716        assert!(result.utilization > 0.0);
717        assert!(result.placements.len() <= 5);
718    }
719
720    #[test]
721    fn test_mass_constraint() {
722        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
723            .with_quantity(10)
724            .with_mass(100.0)];
725
726        let boundary = Boundary3D::new(100.0, 80.0, 50.0).with_max_mass(350.0);
727
728        let packer = Packer3D::default_config();
729        let result = packer.solve(&geometries, &boundary).unwrap();
730
731        // Should only place 3 boxes (300 mass) due to 350 mass limit
732        assert!(result.placements.len() <= 3);
733    }
734
735    #[test]
736    fn test_placement_within_bounds() {
737        let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
738
739        let boundary = Boundary3D::new(50.0, 50.0, 50.0);
740        let config = Config::default().with_margin(5.0).with_spacing(2.0);
741        let packer = Packer3D::new(config);
742
743        let result = packer.solve(&geometries, &boundary).unwrap();
744
745        // All boxes should be placed
746        assert_eq!(result.placements.len(), 4);
747        assert!(result.unplaced.is_empty());
748
749        // Verify placements are within bounds (with margin)
750        for p in &result.placements {
751            assert!(p.position[0] >= 5.0);
752            assert!(p.position[1] >= 5.0);
753            assert!(p.position[2] >= 5.0);
754        }
755    }
756
757    #[test]
758    fn test_ga_strategy_basic() {
759        let geometries = vec![
760            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
761            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
762        ];
763
764        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
765        let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
766        let packer = Packer3D::new(config);
767
768        let result = packer.solve(&geometries, &boundary).unwrap();
769
770        // GA should place items and achieve positive utilization
771        assert!(result.utilization > 0.0);
772        assert!(!result.placements.is_empty());
773    }
774
775    #[test]
776    fn test_ga_strategy_all_placed() {
777        // Small number of boxes that should all fit
778        let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
779
780        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
781        let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
782        let packer = Packer3D::new(config);
783
784        let result = packer.solve(&geometries, &boundary).unwrap();
785
786        // All 4 boxes should be placed
787        assert_eq!(result.placements.len(), 4);
788        assert!(result.unplaced.is_empty());
789    }
790
791    #[test]
792    fn test_ga_strategy_with_orientations() {
793        use crate::geometry::OrientationConstraint;
794
795        // Box that fits better when rotated
796        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
797            .with_quantity(2)
798            .with_orientation(OrientationConstraint::Any)];
799
800        // Container where orientation matters
801        let boundary = Boundary3D::new(60.0, 60.0, 60.0);
802        let config = Config::default().with_strategy(Strategy::GeneticAlgorithm);
803        let packer = Packer3D::new(config);
804
805        let result = packer.solve(&geometries, &boundary).unwrap();
806
807        // GA should find a way to place both boxes
808        assert_eq!(result.placements.len(), 2);
809    }
810
811    #[test]
812    fn test_brkga_strategy_basic() {
813        let geometries = vec![
814            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
815            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
816        ];
817
818        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
819        let config = Config::default().with_strategy(Strategy::Brkga);
820        let packer = Packer3D::new(config);
821
822        let result = packer.solve(&geometries, &boundary).unwrap();
823
824        // BRKGA should place items and achieve positive utilization
825        assert!(result.utilization > 0.0);
826        assert!(!result.placements.is_empty());
827        assert_eq!(result.strategy, Some("BRKGA".to_string()));
828    }
829
830    #[test]
831    fn test_brkga_strategy_all_placed() {
832        // Small number of boxes that should all fit
833        let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
834
835        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
836        let config = Config::default().with_strategy(Strategy::Brkga);
837        let packer = Packer3D::new(config);
838
839        let result = packer.solve(&geometries, &boundary).unwrap();
840
841        // All 4 boxes should be placed
842        assert_eq!(result.placements.len(), 4);
843        assert!(result.unplaced.is_empty());
844    }
845
846    #[test]
847    fn test_ep_strategy_basic() {
848        let geometries = vec![
849            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
850            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
851        ];
852
853        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
854        let config = Config::default().with_strategy(Strategy::ExtremePoint);
855        let packer = Packer3D::new(config);
856
857        let result = packer.solve(&geometries, &boundary).unwrap();
858
859        // EP should place items and achieve positive utilization
860        assert!(result.utilization > 0.0);
861        assert!(!result.placements.is_empty());
862        assert_eq!(result.strategy, Some("ExtremePoint".to_string()));
863    }
864
865    #[test]
866    fn test_ep_strategy_all_placed() {
867        // Small number of boxes that should all fit
868        let geometries = vec![Geometry3D::new("B1", 10.0, 10.0, 10.0).with_quantity(4)];
869
870        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
871        let config = Config::default().with_strategy(Strategy::ExtremePoint);
872        let packer = Packer3D::new(config);
873
874        let result = packer.solve(&geometries, &boundary).unwrap();
875
876        // All 4 boxes should be placed
877        assert_eq!(result.placements.len(), 4);
878        assert!(result.unplaced.is_empty());
879    }
880
881    #[test]
882    fn test_ep_strategy_with_margin() {
883        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
884
885        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
886        let config = Config::default()
887            .with_strategy(Strategy::ExtremePoint)
888            .with_margin(5.0);
889        let packer = Packer3D::new(config);
890
891        let result = packer.solve(&geometries, &boundary).unwrap();
892
893        // Verify placements start at margin
894        for p in &result.placements {
895            assert!(p.position[0] >= 4.9);
896            assert!(p.position[1] >= 4.9);
897            assert!(p.position[2] >= 4.9);
898        }
899    }
900
901    #[test]
902    fn test_ep_strategy_with_orientations() {
903        use crate::geometry::OrientationConstraint;
904
905        // Long box that benefits from rotation
906        let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
907            .with_quantity(2)
908            .with_orientation(OrientationConstraint::Any)];
909
910        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
911        let config = Config::default().with_strategy(Strategy::ExtremePoint);
912        let packer = Packer3D::new(config);
913
914        let result = packer.solve(&geometries, &boundary).unwrap();
915
916        // EP should find a way to place both boxes
917        assert_eq!(result.placements.len(), 2);
918    }
919
920    #[test]
921    fn test_layer_packing_orientation_optimization() {
922        use crate::geometry::OrientationConstraint;
923
924        // A box 50x10x10 that won't fit in 45 width without rotation
925        // But at orientation (1,0,2) it becomes 10x50x10, width=10, which fits
926        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
927            .with_quantity(2)
928            .with_orientation(OrientationConstraint::Any)];
929
930        // Narrow container: width=45, depth=80, height=80
931        let boundary = Boundary3D::new(45.0, 80.0, 80.0);
932        let config = Config::default().with_strategy(Strategy::BottomLeftFill);
933        let packer = Packer3D::new(config);
934
935        let result = packer.solve(&geometries, &boundary).unwrap();
936
937        // Both boxes should be placed via orientation change
938        assert_eq!(
939            result.placements.len(),
940            2,
941            "Both boxes should be placed by using rotation"
942        );
943        assert!(result.unplaced.is_empty());
944
945        // Verify orientation index is set for placements
946        for p in &result.placements {
947            assert!(
948                p.rotation_index.is_some(),
949                "Placement should have rotation_index set"
950            );
951        }
952    }
953
954    #[test]
955    fn test_layer_packing_selects_best_orientation() {
956        use crate::geometry::OrientationConstraint;
957
958        // Box 30x20x10 in container 35x50x100
959        // Original orientation (30x20x10): fits in row, leaves 5 spare width
960        // Rotated (20x30x10): fits but uses more depth
961        // Best: original orientation to minimize vertical space usage
962        let geometries = vec![Geometry3D::new("B1", 30.0, 20.0, 10.0)
963            .with_quantity(1)
964            .with_orientation(OrientationConstraint::Any)];
965
966        let boundary = Boundary3D::new(35.0, 50.0, 100.0);
967        let packer = Packer3D::default_config();
968
969        let result = packer.solve(&geometries, &boundary).unwrap();
970
971        assert_eq!(result.placements.len(), 1);
972        assert!(result.unplaced.is_empty());
973    }
974}