1use crate::boundary::Boundary3D;
19use crate::geometry::Geometry3D;
20use nalgebra::Vector3;
21use std::cmp::Ordering;
22use std::collections::BinaryHeap;
23use u_nesting_core::geometry::{Boundary, Geometry};
24
25#[derive(Debug, Clone, Copy)]
27pub struct ExtremePoint {
28 pub position: Vector3<f64>,
30 pub residual_x: f64,
32 pub residual_y: f64,
34 pub residual_z: f64,
36}
37
38impl ExtremePoint {
39 pub fn new(x: f64, y: f64, z: f64, res_x: f64, res_y: f64, res_z: f64) -> Self {
41 Self {
42 position: Vector3::new(x, y, z),
43 residual_x: res_x,
44 residual_y: res_y,
45 residual_z: res_z,
46 }
47 }
48
49 pub fn pos(&self) -> (f64, f64, f64) {
51 (self.position.x, self.position.y, self.position.z)
52 }
53
54 pub fn fits(&self, width: f64, depth: f64, height: f64) -> bool {
56 width <= self.residual_x + 1e-9
57 && depth <= self.residual_y + 1e-9
58 && height <= self.residual_z + 1e-9
59 }
60}
61
62impl PartialEq for ExtremePoint {
63 fn eq(&self, other: &Self) -> bool {
64 (self.position - other.position).norm() < 1e-9
65 }
66}
67
68impl Eq for ExtremePoint {}
69
70#[derive(Debug, Clone)]
72struct OrderedEP(ExtremePoint);
73
74impl PartialEq for OrderedEP {
75 fn eq(&self, other: &Self) -> bool {
76 self.0 == other.0
77 }
78}
79
80impl Eq for OrderedEP {}
81
82impl PartialOrd for OrderedEP {
83 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
84 Some(self.cmp(other))
85 }
86}
87
88impl Ord for OrderedEP {
89 fn cmp(&self, other: &Self) -> Ordering {
90 let z_cmp = other
92 .0
93 .position
94 .z
95 .partial_cmp(&self.0.position.z)
96 .unwrap_or(Ordering::Equal);
97 if z_cmp != Ordering::Equal {
98 return z_cmp;
99 }
100
101 let y_cmp = other
102 .0
103 .position
104 .y
105 .partial_cmp(&self.0.position.y)
106 .unwrap_or(Ordering::Equal);
107 if y_cmp != Ordering::Equal {
108 return y_cmp;
109 }
110
111 other
112 .0
113 .position
114 .x
115 .partial_cmp(&self.0.position.x)
116 .unwrap_or(Ordering::Equal)
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct PlacedBox {
123 pub id: String,
125 pub instance: usize,
127 pub position: Vector3<f64>,
129 pub dimensions: Vector3<f64>,
131 pub mass: Option<f64>,
133}
134
135impl PlacedBox {
136 pub fn max_corner(&self) -> Vector3<f64> {
138 self.position + self.dimensions
139 }
140
141 pub fn overlaps(&self, other: &PlacedBox) -> bool {
143 let self_max = self.max_corner();
144 let other_max = other.max_corner();
145
146 let no_overlap_x =
148 self.position.x >= other_max.x - 1e-9 || other.position.x >= self_max.x - 1e-9;
149 let no_overlap_y =
150 self.position.y >= other_max.y - 1e-9 || other.position.y >= self_max.y - 1e-9;
151 let no_overlap_z =
152 self.position.z >= other_max.z - 1e-9 || other.position.z >= self_max.z - 1e-9;
153
154 !(no_overlap_x || no_overlap_y || no_overlap_z)
155 }
156}
157
158pub struct ExtremePointSet {
160 points: BinaryHeap<OrderedEP>,
162 container: Vector3<f64>,
164 placed: Vec<PlacedBox>,
166 spacing: f64,
168 margin: f64,
170}
171
172impl ExtremePointSet {
173 pub fn new(boundary: &Boundary3D, margin: f64, spacing: f64) -> Self {
175 let container = Vector3::new(boundary.width(), boundary.depth(), boundary.height());
176
177 let mut eps = Self {
178 points: BinaryHeap::new(),
179 container,
180 placed: Vec::new(),
181 spacing,
182 margin,
183 };
184
185 let initial_ep = ExtremePoint::new(
187 margin,
188 margin,
189 margin,
190 container.x - 2.0 * margin,
191 container.y - 2.0 * margin,
192 container.z - 2.0 * margin,
193 );
194 eps.points.push(OrderedEP(initial_ep));
195
196 eps
197 }
198
199 pub fn len(&self) -> usize {
201 self.points.len()
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.points.is_empty()
207 }
208
209 pub fn placed_count(&self) -> usize {
211 self.placed.len()
212 }
213
214 pub fn total_volume(&self) -> f64 {
216 self.placed
217 .iter()
218 .map(|b| b.dimensions.x * b.dimensions.y * b.dimensions.z)
219 .sum()
220 }
221
222 pub fn total_mass(&self) -> f64 {
224 self.placed.iter().filter_map(|b| b.mass).sum()
225 }
226
227 pub fn placed_boxes(&self) -> &[PlacedBox] {
229 &self.placed
230 }
231
232 pub fn try_place(
234 &mut self,
235 geom: &Geometry3D,
236 instance: usize,
237 orientation: usize,
238 ) -> Option<Vector3<f64>> {
239 let dims = geom.dimensions_for_orientation(orientation);
240 let width = dims.x + self.spacing;
241 let depth = dims.y + self.spacing;
242 let height = dims.z + self.spacing;
243
244 let mut candidates: Vec<ExtremePoint> = Vec::new();
246 while let Some(OrderedEP(ep)) = self.points.pop() {
247 candidates.push(ep);
248 }
249
250 let mut best_ep_idx: Option<usize> = None;
252 for (idx, ep) in candidates.iter().enumerate() {
253 if ep.fits(width, height, depth) || ep.fits(width, depth, height) {
254 let test_box = PlacedBox {
256 id: String::new(),
257 instance: 0,
258 position: ep.position,
259 dimensions: dims,
260 mass: None,
261 };
262
263 let overlaps = self.placed.iter().any(|placed| test_box.overlaps(placed));
264 if !overlaps {
265 best_ep_idx = Some(idx);
266 break;
267 }
268 }
269 }
270
271 let result = if let Some(idx) = best_ep_idx {
273 let chosen_ep = candidates.remove(idx);
274
275 let placed_box = PlacedBox {
277 id: geom.id().clone(),
278 instance,
279 position: chosen_ep.position,
280 dimensions: dims,
281 mass: geom.mass(),
282 };
283
284 self.generate_new_eps(&placed_box);
286
287 let position = chosen_ep.position;
288 self.placed.push(placed_box);
289
290 Some(position)
291 } else {
292 None
293 };
294
295 for ep in candidates {
297 self.points.push(OrderedEP(ep));
298 }
299
300 result
301 }
302
303 fn generate_new_eps(&mut self, placed: &PlacedBox) {
305 let box_max = placed.max_corner();
306 let container_max = self.container - Vector3::new(self.margin, self.margin, self.margin);
307
308 if box_max.x < container_max.x {
310 let res_x = container_max.x - box_max.x;
311 let res_y = self.compute_residual_y(box_max.x, placed.position.y, placed.position.z);
312 let res_z = self.compute_residual_z(box_max.x, placed.position.y, placed.position.z);
313
314 if res_x > 1e-9 && res_y > 1e-9 && res_z > 1e-9 {
315 let ep = ExtremePoint::new(
316 box_max.x,
317 placed.position.y,
318 placed.position.z,
319 res_x,
320 res_y,
321 res_z,
322 );
323 self.add_ep_if_valid(ep);
324 }
325 }
326
327 if box_max.y < container_max.y {
329 let res_x = self.compute_residual_x(placed.position.x, box_max.y, placed.position.z);
330 let res_y = container_max.y - box_max.y;
331 let res_z = self.compute_residual_z(placed.position.x, box_max.y, placed.position.z);
332
333 if res_x > 1e-9 && res_y > 1e-9 && res_z > 1e-9 {
334 let ep = ExtremePoint::new(
335 placed.position.x,
336 box_max.y,
337 placed.position.z,
338 res_x,
339 res_y,
340 res_z,
341 );
342 self.add_ep_if_valid(ep);
343 }
344 }
345
346 if box_max.z < container_max.z {
348 let res_x = self.compute_residual_x(placed.position.x, placed.position.y, box_max.z);
349 let res_y = self.compute_residual_y(placed.position.x, placed.position.y, box_max.z);
350 let res_z = container_max.z - box_max.z;
351
352 if res_x > 1e-9 && res_y > 1e-9 && res_z > 1e-9 {
353 let ep = ExtremePoint::new(
354 placed.position.x,
355 placed.position.y,
356 box_max.z,
357 res_x,
358 res_y,
359 res_z,
360 );
361 self.add_ep_if_valid(ep);
362 }
363 }
364 }
365
366 fn add_ep_if_valid(&mut self, ep: ExtremePoint) {
368 let container_max = self.container - Vector3::new(self.margin, self.margin, self.margin);
370 if ep.position.x >= container_max.x - 1e-9
371 || ep.position.y >= container_max.y - 1e-9
372 || ep.position.z >= container_max.z - 1e-9
373 {
374 return;
375 }
376
377 for placed in &self.placed {
379 let max = placed.max_corner();
380 if ep.position.x > placed.position.x - 1e-9
381 && ep.position.x < max.x + 1e-9
382 && ep.position.y > placed.position.y - 1e-9
383 && ep.position.y < max.y + 1e-9
384 && ep.position.z > placed.position.z - 1e-9
385 && ep.position.z < max.z + 1e-9
386 {
387 return;
388 }
389 }
390
391 self.points.push(OrderedEP(ep));
392 }
393
394 fn compute_residual_x(&self, x: f64, y: f64, z: f64) -> f64 {
396 let container_max_x = self.container.x - self.margin;
397 let mut min_x = container_max_x;
398
399 for placed in &self.placed {
400 let p_max = placed.max_corner();
401 if placed.position.y < y + 1e-9
403 && p_max.y > y - 1e-9
404 && placed.position.z < z + 1e-9
405 && p_max.z > z - 1e-9
406 && placed.position.x > x - 1e-9
407 && placed.position.x < min_x
408 {
409 min_x = placed.position.x;
410 }
411 }
412
413 (min_x - x).max(0.0)
414 }
415
416 fn compute_residual_y(&self, x: f64, y: f64, z: f64) -> f64 {
418 let container_max_y = self.container.y - self.margin;
419 let mut min_y = container_max_y;
420
421 for placed in &self.placed {
422 let p_max = placed.max_corner();
423 if placed.position.x < x + 1e-9
425 && p_max.x > x - 1e-9
426 && placed.position.z < z + 1e-9
427 && p_max.z > z - 1e-9
428 && placed.position.y > y - 1e-9
429 && placed.position.y < min_y
430 {
431 min_y = placed.position.y;
432 }
433 }
434
435 (min_y - y).max(0.0)
436 }
437
438 fn compute_residual_z(&self, x: f64, y: f64, z: f64) -> f64 {
440 let container_max_z = self.container.z - self.margin;
441 let mut min_z = container_max_z;
442
443 for placed in &self.placed {
444 let p_max = placed.max_corner();
445 if placed.position.x < x + 1e-9
447 && p_max.x > x - 1e-9
448 && placed.position.y < y + 1e-9
449 && p_max.y > y - 1e-9
450 && placed.position.z > z - 1e-9
451 && placed.position.z < min_z
452 {
453 min_z = placed.position.z;
454 }
455 }
456
457 (min_z - z).max(0.0)
458 }
459}
460
461#[derive(Debug, Clone, Copy, Default)]
463pub enum EpSelectionStrategy {
464 #[default]
466 BottomLeftBack,
467 BestFit,
469 FirstFit,
471}
472
473pub type EpPlacement = (String, usize, Vector3<f64>, usize);
475
476pub fn run_ep_packing(
478 geometries: &[Geometry3D],
479 boundary: &Boundary3D,
480 margin: f64,
481 spacing: f64,
482 max_mass: Option<f64>,
483) -> (Vec<EpPlacement>, f64) {
484 let mut eps = ExtremePointSet::new(boundary, margin, spacing);
485 let mut placements = Vec::new();
486
487 let mut items: Vec<(usize, usize, f64)> = Vec::new();
489 for (geom_idx, geom) in geometries.iter().enumerate() {
490 for instance in 0..geom.quantity() {
491 items.push((geom_idx, instance, geom.measure()));
492 }
493 }
494 items.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(Ordering::Equal));
495
496 for (geom_idx, instance, _) in items {
497 let geom = &geometries[geom_idx];
498
499 if let (Some(max), Some(item_mass)) = (max_mass, geom.mass()) {
501 if eps.total_mass() + item_mass > max {
502 continue;
503 }
504 }
505
506 let mut placed = false;
508 for orientation in 0..geom.allowed_orientations().len() {
509 if let Some(position) = eps.try_place(geom, instance, orientation) {
510 placements.push((geom.id().clone(), instance, position, orientation));
511 placed = true;
512 break;
513 }
514 }
515
516 if !placed {
517 }
519 }
520
521 let utilization = eps.total_volume() / boundary.measure();
522 (placements, utilization)
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn test_extreme_point_creation() {
531 let ep = ExtremePoint::new(0.0, 0.0, 0.0, 100.0, 100.0, 100.0);
532 assert!(ep.fits(50.0, 50.0, 50.0));
533 assert!(!ep.fits(150.0, 50.0, 50.0));
534 }
535
536 #[test]
537 fn test_extreme_point_set_initial() {
538 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
539 let eps = ExtremePointSet::new(&boundary, 0.0, 0.0);
540
541 assert_eq!(eps.len(), 1);
542 assert!(eps.is_empty() == false);
543 }
544
545 #[test]
546 fn test_ep_packing_single_box() {
547 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)];
548 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
549
550 let (placements, utilization) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
551
552 assert_eq!(placements.len(), 1);
553 assert!(utilization > 0.0);
554 }
555
556 #[test]
557 fn test_ep_packing_multiple_boxes() {
558 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(8)];
559 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
560
561 let (placements, utilization) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
562
563 assert!(placements.len() >= 4);
565 assert!(utilization > 0.05);
566 }
567
568 #[test]
569 fn test_ep_packing_with_margin() {
570 let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
571 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
572
573 let (placements, _) = run_ep_packing(&geometries, &boundary, 5.0, 0.0, None);
574
575 if !placements.is_empty() {
577 let (_, _, pos, _) = &placements[0];
578 assert!(pos.x >= 4.9);
579 assert!(pos.y >= 4.9);
580 assert!(pos.z >= 4.9);
581 }
582 }
583
584 #[test]
585 fn test_ep_packing_with_spacing() {
586 let geometries = vec![Geometry3D::new("B1", 40.0, 40.0, 40.0).with_quantity(4)];
587 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
588
589 let (placements_no_spacing, _) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
590 let (placements_with_spacing, _) = run_ep_packing(&geometries, &boundary, 0.0, 5.0, None);
591
592 assert!(placements_with_spacing.len() <= placements_no_spacing.len());
594 }
595
596 #[test]
597 fn test_placed_box_overlap() {
598 let box1 = PlacedBox {
599 id: "A".to_string(),
600 instance: 0,
601 position: Vector3::new(0.0, 0.0, 0.0),
602 dimensions: Vector3::new(10.0, 10.0, 10.0),
603 mass: None,
604 };
605
606 let box2_overlap = PlacedBox {
607 id: "B".to_string(),
608 instance: 0,
609 position: Vector3::new(5.0, 5.0, 5.0),
610 dimensions: Vector3::new(10.0, 10.0, 10.0),
611 mass: None,
612 };
613
614 let box2_no_overlap = PlacedBox {
615 id: "C".to_string(),
616 instance: 0,
617 position: Vector3::new(15.0, 0.0, 0.0),
618 dimensions: Vector3::new(10.0, 10.0, 10.0),
619 mass: None,
620 };
621
622 assert!(box1.overlaps(&box2_overlap));
623 assert!(!box1.overlaps(&box2_no_overlap));
624 }
625
626 #[test]
627 fn test_ep_packing_orientations() {
628 use crate::geometry::OrientationConstraint;
629
630 let geometries = vec![Geometry3D::new("B1", 80.0, 10.0, 10.0)
632 .with_quantity(2)
633 .with_orientation(OrientationConstraint::Any)];
634 let boundary = Boundary3D::new(100.0, 100.0, 100.0);
635
636 let (placements, _) = run_ep_packing(&geometries, &boundary, 0.0, 0.0, None);
637
638 assert_eq!(placements.len(), 2);
640 }
641}