rectangle_pack/
lib.rs

1//! `rectangle-pack` is a library focused on laying out any number of smaller rectangles
2//! (both 2d rectangles and 3d rectangular prisms) inside any number of larger rectangles.
3#![cfg_attr(not(std), no_std)]
4#![deny(missing_docs)]
5
6#[macro_use]
7extern crate alloc;
8
9#[cfg(not(std))]
10use alloc::collections::BTreeMap as KeyValMap;
11#[cfg(std)]
12use std::collections::HashMap as KeyValMap;
13
14use alloc::{collections::BTreeMap, vec::Vec};
15
16use core::{
17    fmt::{Debug, Display, Error as FmtError, Formatter},
18    hash::Hash,
19};
20
21pub use crate::bin_section::contains_smallest_box;
22pub use crate::bin_section::BinSection;
23pub use crate::bin_section::ComparePotentialContainersFn;
24use crate::grouped_rects_to_place::Group;
25pub use crate::grouped_rects_to_place::GroupedRectsToPlace;
26pub use crate::target_bin::TargetBin;
27use crate::width_height_depth::WidthHeightDepth;
28
29pub use self::box_size_heuristics::{volume_heuristic, BoxSizeHeuristicFn};
30pub use self::rect_to_insert::RectToInsert;
31pub use crate::packed_location::PackedLocation;
32
33mod bin_section;
34mod grouped_rects_to_place;
35
36mod packed_location;
37mod rect_to_insert;
38mod target_bin;
39mod width_height_depth;
40
41mod box_size_heuristics;
42
43/// Determine how to fit a set of incoming rectangles (2d or 3d) into a set of target bins.
44///
45/// ## Example
46///
47/// ```
48/// //! A basic example of packing rectangles into target bins
49///
50/// use rectangle_pack::{
51///     GroupedRectsToPlace,
52///     RectToInsert,
53///     pack_rects,
54///     TargetBin,
55///     volume_heuristic,
56///     contains_smallest_box
57/// };
58/// use std::collections::BTreeMap;
59///
60/// // A rectangle ID just needs to meet these trait bounds (ideally also Copy).
61/// // So you could use a String, PathBuf, or any other type that meets these
62/// // trat bounds. You do not have to use a custom enum.
63/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
64/// enum MyCustomRectId {
65///     RectOne,
66///     RectTwo,
67///     RectThree,
68/// }
69///
70/// // A target bin ID just needs to meet these trait bounds (ideally also Copy)
71/// // So you could use a u32, &str, or any other type that meets these
72/// // trat bounds. You do not have to use a custom enum.
73/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
74/// enum MyCustomBinId {
75///     DestinationBinOne,
76///     DestinationBinTwo,
77/// }
78///
79/// // A placement group just needs to meet these trait bounds (ideally also Copy).
80/// //
81/// // Groups allow you to ensure that a set of rectangles will be placed
82/// // into the same bin. If this isn't possible an error is returned.
83/// //
84/// // Groups are optional.
85/// //
86/// // You could use an i32, &'static str, or any other type that meets these
87/// // trat bounds. You do not have to use a custom enum.
88/// #[derive(Debug, Hash, PartialEq, Eq, Clone, Ord, PartialOrd)]
89/// enum MyCustomGroupId {
90///     GroupIdOne
91/// }
92///
93/// let mut rects_to_place = GroupedRectsToPlace::new();
94/// rects_to_place.push_rect(
95///     MyCustomRectId::RectOne,
96///     Some(vec![MyCustomGroupId::GroupIdOne]),
97///     RectToInsert::new(10, 20, 255)
98/// );
99/// rects_to_place.push_rect(
100///     MyCustomRectId::RectTwo,
101///     Some(vec![MyCustomGroupId::GroupIdOne]),
102///     RectToInsert::new(5, 50, 255)
103/// );
104/// rects_to_place.push_rect(
105///     MyCustomRectId::RectThree,
106///     None,
107///     RectToInsert::new(30, 30, 255)
108/// );
109///
110/// let mut target_bins = BTreeMap::new();
111/// target_bins.insert(MyCustomBinId::DestinationBinOne, TargetBin::new(2048, 2048, 255));
112/// target_bins.insert(MyCustomBinId::DestinationBinTwo, TargetBin::new(4096, 4096, 1020));
113///
114/// // Information about where each `MyCustomRectId` was placed
115/// let rectangle_placements = pack_rects(
116///     &rects_to_place,
117///     &mut target_bins,
118///     &volume_heuristic,
119///     &contains_smallest_box
120/// ).unwrap();
121/// ```
122///
123/// ## Algorithm
124///
125/// The algorithm was originally inspired by [rectpack2D] and then modified to work in 3D.
126///
127/// [rectpack2D]: https://github.com/TeamHypersomnia/rectpack2D
128///
129/// ## TODO:
130///
131/// Optimize - plenty of room to remove clones and duplication .. etc
132pub fn pack_rects<
133    RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
134    BinId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
135    GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
136>(
137    rects_to_place: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
138    target_bins: &mut BTreeMap<BinId, TargetBin>,
139    box_size_heuristic: &BoxSizeHeuristicFn,
140    more_suitable_containers_fn: &ComparePotentialContainersFn,
141) -> Result<RectanglePackOk<RectToPlaceId, BinId>, RectanglePackError> {
142    let mut packed_locations = KeyValMap::new();
143
144    let mut target_bins: Vec<(&BinId, &mut TargetBin)> = target_bins.iter_mut().collect();
145    sort_bins_smallest_to_largest(&mut target_bins, box_size_heuristic);
146
147    let mut group_id_to_inbound_ids: Vec<(&Group<GroupId, RectToPlaceId>, &Vec<RectToPlaceId>)> =
148        rects_to_place.group_id_to_inbound_ids.iter().collect();
149    sort_groups_largest_to_smallest(
150        &mut group_id_to_inbound_ids,
151        rects_to_place,
152        box_size_heuristic,
153    );
154
155    'group: for (_group_id, rects_to_place_ids) in group_id_to_inbound_ids {
156        for (bin_id, bin) in target_bins.iter_mut() {
157            if !can_fit_entire_group_into_bin(
158                bin.clone(),
159                &rects_to_place_ids[..],
160                rects_to_place,
161                box_size_heuristic,
162                more_suitable_containers_fn,
163            ) {
164                continue;
165            }
166
167            'incoming: for rect_to_place_id in rects_to_place_ids.iter() {
168                if bin.available_bin_sections.len() == 0 {
169                    continue;
170                }
171
172                let _bin_clone = bin.clone();
173
174                let mut bin_sections = bin.available_bin_sections.clone();
175
176                let last_section_idx = bin_sections.len() - 1;
177                let mut sections_tried = 0;
178
179                'section: while let Some(remaining_section) = bin_sections.pop() {
180                    let rect_to_place = rects_to_place.rects[&rect_to_place_id];
181
182                    let placement = remaining_section.try_place(
183                        &rect_to_place,
184                        more_suitable_containers_fn,
185                        box_size_heuristic,
186                    );
187
188                    if placement.is_err() {
189                        sections_tried += 1;
190                        continue 'section;
191                    }
192
193                    let (placement, mut new_sections) = placement.unwrap();
194                    sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic);
195
196                    bin.remove_filled_section(last_section_idx - sections_tried);
197                    bin.add_new_sections(new_sections);
198
199                    packed_locations.insert(rect_to_place_id.clone(), (bin_id.clone(), placement));
200
201                    continue 'incoming;
202                }
203            }
204
205            continue 'group;
206        }
207        return Err(RectanglePackError::NotEnoughBinSpace);
208    }
209
210    Ok(RectanglePackOk { packed_locations })
211}
212
213// TODO: This is duplicative of the code above
214fn can_fit_entire_group_into_bin<RectToPlaceId, GroupId>(
215    mut bin: TargetBin,
216    group: &[RectToPlaceId],
217    rects_to_place: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
218
219    box_size_heuristic: &BoxSizeHeuristicFn,
220    more_suitable_containers_fn: &ComparePotentialContainersFn,
221) -> bool
222where
223    RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
224    GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
225{
226    'incoming: for rect_to_place_id in group.iter() {
227        if bin.available_bin_sections.len() == 0 {
228            return false;
229        }
230
231        let mut bin_sections = bin.available_bin_sections.clone();
232
233        let last_section_idx = bin_sections.len() - 1;
234        let mut sections_tried = 0;
235
236        'section: while let Some(remaining_section) = bin_sections.pop() {
237            let rect_to_place = rects_to_place.rects[&rect_to_place_id];
238
239            let placement = remaining_section.try_place(
240                &rect_to_place,
241                more_suitable_containers_fn,
242                box_size_heuristic,
243            );
244
245            if placement.is_err() {
246                sections_tried += 1;
247                continue 'section;
248            }
249
250            let (_placement, mut new_sections) = placement.unwrap();
251            sort_by_size_largest_to_smallest(&mut new_sections, box_size_heuristic);
252
253            bin.remove_filled_section(last_section_idx - sections_tried);
254            bin.add_new_sections(new_sections);
255
256            continue 'incoming;
257        }
258
259        return false;
260    }
261
262    true
263}
264
265/// Information about successfully packed rectangles.
266#[derive(Debug, PartialEq)]
267pub struct RectanglePackOk<RectToPlaceId: PartialEq + Eq + Hash, BinId: PartialEq + Eq + Hash> {
268    packed_locations: KeyValMap<RectToPlaceId, (BinId, PackedLocation)>,
269    // TODO: Other information such as information about how the bins were packed
270    // (perhaps percentage filled)
271}
272
273impl<RectToPlaceId: PartialEq + Eq + Hash, BinId: PartialEq + Eq + Hash>
274    RectanglePackOk<RectToPlaceId, BinId>
275{
276    /// Indicates where every incoming rectangle was placed
277    pub fn packed_locations(&self) -> &KeyValMap<RectToPlaceId, (BinId, PackedLocation)> {
278        &self.packed_locations
279    }
280}
281
282/// An error while attempting to pack rectangles into bins.
283#[derive(Debug, PartialEq)]
284pub enum RectanglePackError {
285    /// The rectangles can't be placed into the bins. More bin space needs to be provided.
286    NotEnoughBinSpace,
287}
288
289#[cfg(std)]
290impl std::error::Error for RectanglePackError {}
291
292impl Display for RectanglePackError {
293    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
294        match self {
295            RectanglePackError::NotEnoughBinSpace => {
296                f.write_str("Not enough space to place all of the rectangles.")
297            }
298        }
299    }
300}
301
302fn sort_bins_smallest_to_largest<BinId>(
303    bins: &mut Vec<(&BinId, &mut TargetBin)>,
304    box_size_heuristic: &BoxSizeHeuristicFn,
305) where
306    BinId: Debug + Hash + PartialEq + Eq + Clone,
307{
308    bins.sort_by(|a, b| {
309        box_size_heuristic(WidthHeightDepth {
310            width: a.1.max_width,
311            height: a.1.max_height,
312            depth: a.1.max_depth,
313        })
314        .cmp(&box_size_heuristic(WidthHeightDepth {
315            width: b.1.max_width,
316            height: b.1.max_height,
317            depth: b.1.max_depth,
318        }))
319    });
320}
321
322fn sort_by_size_largest_to_smallest(
323    items: &mut [BinSection; 3],
324    box_size_heuristic: &BoxSizeHeuristicFn,
325) {
326    items.sort_by(|a, b| box_size_heuristic(b.whd).cmp(&box_size_heuristic(a.whd)));
327}
328
329fn sort_groups_largest_to_smallest<GroupId, RectToPlaceId>(
330    group_id_to_inbound_ids: &mut Vec<(&Group<GroupId, RectToPlaceId>, &Vec<RectToPlaceId>)>,
331    incoming_groups: &GroupedRectsToPlace<RectToPlaceId, GroupId>,
332    box_size_heuristic: &BoxSizeHeuristicFn,
333) where
334    RectToPlaceId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
335    GroupId: Debug + Hash + PartialEq + Eq + Clone + Ord + PartialOrd,
336{
337    group_id_to_inbound_ids.sort_by(|a, b| {
338        let a_heuristic =
339            a.1.iter()
340                .map(|inbound| {
341                    let rect = incoming_groups.rects[inbound];
342                    box_size_heuristic(rect.whd)
343                })
344                .sum();
345
346        let b_heuristic: u128 =
347            b.1.iter()
348                .map(|inbound| {
349                    let rect = incoming_groups.rects[inbound];
350                    box_size_heuristic(rect.whd)
351                })
352                .sum();
353
354        b_heuristic.cmp(&a_heuristic)
355    });
356}
357
358#[cfg(test)]
359mod tests {
360    use crate::{pack_rects, volume_heuristic, RectToInsert, RectanglePackError, TargetBin};
361
362    use super::*;
363    use crate::packed_location::RotatedBy;
364
365    /// If the provided rectangles can't fit into the provided bins.
366    #[test]
367    fn error_if_the_rectangles_cannot_fit_into_target_bins() {
368        let mut targets = BTreeMap::new();
369        targets.insert(BinId::Three, TargetBin::new(2, 100, 1));
370
371        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
372        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(3, 1, 1));
373
374        match pack_rects(
375            &groups,
376            &mut targets,
377            &volume_heuristic,
378            &contains_smallest_box,
379        )
380        .unwrap_err()
381        {
382            RectanglePackError::NotEnoughBinSpace => {}
383        };
384    }
385
386    /// Rectangles in the same group need to be placed in the same bin.
387    ///
388    /// Here we create two Rectangles in the same group and create two bins that could fit them
389    /// individually but cannot fit them together.
390    ///
391    /// Then we verify that we receive an error for being unable to place the group.
392    #[test]
393    fn error_if_cannot_fit_group() {
394        let mut targets = BTreeMap::new();
395        targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
396        targets.insert(BinId::Four, TargetBin::new(100, 100, 1));
397
398        let mut groups = GroupedRectsToPlace::new();
399        groups.push_rect(
400            RectToPlaceId::One,
401            Some(vec!["A Group"]),
402            RectToInsert::new(100, 100, 1),
403        );
404        groups.push_rect(
405            RectToPlaceId::Two,
406            Some(vec!["A Group"]),
407            RectToInsert::new(100, 100, 1),
408        );
409
410        match pack_rects(
411            &groups,
412            &mut targets,
413            &volume_heuristic,
414            &contains_smallest_box,
415        )
416        .unwrap_err()
417        {
418            RectanglePackError::NotEnoughBinSpace => {}
419        };
420    }
421
422    /// If we provide a single inbound rectangle and a single bin - it should be placed into that
423    /// bin.
424    #[test]
425    fn one_inbound_rect_one_bin() {
426        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
427        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(1, 2, 1));
428
429        let mut targets = BTreeMap::new();
430        targets.insert(BinId::Three, TargetBin::new(5, 5, 1));
431
432        let packed = pack_rects(
433            &groups,
434            &mut targets,
435            &volume_heuristic,
436            &contains_smallest_box,
437        )
438        .unwrap();
439        let locations = packed.packed_locations;
440
441        assert_eq!(locations.len(), 1);
442
443        assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
444        assert_eq!(
445            locations[&RectToPlaceId::One].1,
446            PackedLocation {
447                x: 0,
448                y: 0,
449                z: 0,
450                whd: WidthHeightDepth {
451                    width: 1,
452                    height: 2,
453                    depth: 1
454                },
455                x_axis_rotation: RotatedBy::ZeroDegrees,
456                y_axis_rotation: RotatedBy::ZeroDegrees,
457                z_axis_rotation: RotatedBy::ZeroDegrees,
458            }
459        )
460    }
461
462    /// If we have one inbound rect and two bins, it should be placed into the smallest bin.
463    #[test]
464    fn one_inbound_rect_two_bins() {
465        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
466        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(2, 2, 1));
467
468        let mut targets = BTreeMap::new();
469        targets.insert(BinId::Three, TargetBin::new(5, 5, 1));
470        targets.insert(BinId::Four, TargetBin::new(5, 5, 2));
471
472        let packed = pack_rects(
473            &groups,
474            &mut targets,
475            &volume_heuristic,
476            &contains_smallest_box,
477        )
478        .unwrap();
479        let locations = packed.packed_locations;
480
481        assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
482
483        assert_eq!(locations.len(), 1);
484        assert_eq!(
485            locations[&RectToPlaceId::One].1,
486            PackedLocation {
487                x: 0,
488                y: 0,
489                z: 0,
490                whd: WidthHeightDepth {
491                    width: 2,
492                    height: 2,
493                    depth: 1
494                },
495                x_axis_rotation: RotatedBy::ZeroDegrees,
496                y_axis_rotation: RotatedBy::ZeroDegrees,
497                z_axis_rotation: RotatedBy::ZeroDegrees,
498            }
499        )
500    }
501
502    /// If we have two inbound rects the largest one should be placed first.
503    #[test]
504    fn places_largest_rectangles_first() {
505        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
506        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(10, 10, 1));
507        groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(5, 5, 1));
508
509        let mut targets = BTreeMap::new();
510        targets.insert(BinId::Three, TargetBin::new(20, 20, 2));
511
512        let packed = pack_rects(
513            &groups,
514            &mut targets,
515            &volume_heuristic,
516            &contains_smallest_box,
517        )
518        .unwrap();
519        let locations = packed.packed_locations;
520
521        assert_eq!(locations.len(), 2);
522
523        assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
524        assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
525
526        assert_eq!(
527            locations[&RectToPlaceId::One].1,
528            PackedLocation {
529                x: 0,
530                y: 0,
531                z: 0,
532                whd: WidthHeightDepth {
533                    width: 10,
534                    height: 10,
535                    depth: 1
536                },
537                x_axis_rotation: RotatedBy::ZeroDegrees,
538                y_axis_rotation: RotatedBy::ZeroDegrees,
539                z_axis_rotation: RotatedBy::ZeroDegrees,
540            }
541        );
542        assert_eq!(
543            locations[&RectToPlaceId::Two].1,
544            PackedLocation {
545                x: 10,
546                y: 0,
547                z: 0,
548                whd: WidthHeightDepth {
549                    width: 5,
550                    height: 5,
551                    depth: 1
552                },
553                x_axis_rotation: RotatedBy::ZeroDegrees,
554                y_axis_rotation: RotatedBy::ZeroDegrees,
555                z_axis_rotation: RotatedBy::ZeroDegrees,
556            }
557        )
558    }
559
560    /// We have two rectangles and two bins. Each bin has enough space to fit one rectangle.
561    ///
562    /// 1. First place the largest rectangle into the smallest bin.
563    ///
564    /// 2. Second place the remaining rectangle into the next available bin (i.e. the largest one).
565    #[test]
566    fn two_rects_two_bins() {
567        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
568        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(15, 15, 1));
569        groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(20, 20, 1));
570
571        let mut targets = BTreeMap::new();
572        targets.insert(BinId::Three, TargetBin::new(20, 20, 1));
573        targets.insert(BinId::Four, TargetBin::new(50, 50, 1));
574
575        let packed = pack_rects(
576            &groups,
577            &mut targets,
578            &volume_heuristic,
579            &contains_smallest_box,
580        )
581        .unwrap();
582        let locations = packed.packed_locations;
583
584        assert_eq!(locations.len(), 2);
585
586        assert_eq!(locations[&RectToPlaceId::One].0, BinId::Four,);
587        assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
588
589        assert_eq!(
590            locations[&RectToPlaceId::One].1,
591            PackedLocation {
592                x: 0,
593                y: 0,
594                z: 0,
595                whd: WidthHeightDepth {
596                    width: 15,
597                    height: 15,
598                    depth: 1
599                },
600                x_axis_rotation: RotatedBy::ZeroDegrees,
601                y_axis_rotation: RotatedBy::ZeroDegrees,
602                z_axis_rotation: RotatedBy::ZeroDegrees,
603            }
604        );
605        assert_eq!(
606            locations[&RectToPlaceId::Two].1,
607            PackedLocation {
608                x: 0,
609                y: 0,
610                z: 0,
611                whd: WidthHeightDepth {
612                    width: 20,
613                    height: 20,
614                    depth: 1
615                },
616                x_axis_rotation: RotatedBy::ZeroDegrees,
617                y_axis_rotation: RotatedBy::ZeroDegrees,
618                z_axis_rotation: RotatedBy::ZeroDegrees,
619            }
620        )
621    }
622
623    /// If there are two sections available to fill - the smaller one should be filled first
624    /// (if possible).
625    ///
626    /// We test this by creating two incoming rectangles.
627    ///
628    /// The largest one is placed and creates two new sections - after which the second, smaller one
629    /// should get placed into the smaller of the two new sections.
630    ///
631    /// ```text
632    /// ┌──────────────┬──▲───────────────┐
633    /// │ Second Rect  │  │               │
634    /// ├──────────────┴──┤               │
635    /// │                 │               │
636    /// │  First Placed   │               │
637    /// │    Rectangle    │               │
638    /// │                 │               │
639    /// └─────────────────┴───────────────┘
640    /// ```
641    #[test]
642    fn fills_small_sections_before_large_ones() {
643        let mut targets = BTreeMap::new();
644        targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
645
646        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
647
648        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(50, 90, 1));
649        groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(1, 1, 1));
650
651        let packed = pack_rects(
652            &groups,
653            &mut targets,
654            &volume_heuristic,
655            &contains_smallest_box,
656        )
657        .unwrap();
658        let locations = packed.packed_locations;
659
660        assert_eq!(locations.len(), 2);
661
662        assert_eq!(locations[&RectToPlaceId::One].0, BinId::Three,);
663        assert_eq!(locations[&RectToPlaceId::Two].0, BinId::Three,);
664
665        assert_eq!(
666            locations[&RectToPlaceId::One].1,
667            PackedLocation {
668                x: 0,
669                y: 0,
670                z: 0,
671                whd: WidthHeightDepth {
672                    width: 50,
673                    height: 90,
674                    depth: 1
675                },
676                x_axis_rotation: RotatedBy::ZeroDegrees,
677                y_axis_rotation: RotatedBy::ZeroDegrees,
678                z_axis_rotation: RotatedBy::ZeroDegrees,
679            }
680        );
681        assert_eq!(
682            locations[&RectToPlaceId::Two].1,
683            PackedLocation {
684                x: 0,
685                y: 90,
686                z: 0,
687                whd: WidthHeightDepth {
688                    width: 1,
689                    height: 1,
690                    depth: 1
691                },
692                x_axis_rotation: RotatedBy::ZeroDegrees,
693                y_axis_rotation: RotatedBy::ZeroDegrees,
694                z_axis_rotation: RotatedBy::ZeroDegrees,
695            }
696        );
697    }
698
699    /// Say we have one bin and three rectangles to place within in.
700    ///
701    /// The first one gets placed and creates two new splits.
702    ///
703    /// We then attempt to place the second one into the smallest split. It's too big to fit, so
704    /// we place it into the largest split.
705    ///
706    /// After that we place the third rectangle into the smallest split.
707    ///
708    /// Here we verify that that actually occurs and that we didn't throw away that smallest split
709    /// when the second one couldn't fit in it.
710    ///
711    /// ```text
712    /// ┌──────────────┬──────────────┐
713    /// │    Third     │              │
714    /// ├──────────────┤              │
715    /// │              │              │
716    /// │              │              │
717    /// │              ├──────────────┤
718    /// │   First      │              │
719    /// │              │    Second    │
720    /// │              │              │
721    /// └──────────────┴──────────────┘
722    /// ```
723    #[test]
724    fn saves_bin_sections_for_future_use() {
725        let mut targets = BTreeMap::new();
726        targets.insert(BinId::Three, TargetBin::new(100, 100, 1));
727
728        let mut groups: GroupedRectsToPlace<_, ()> = GroupedRectsToPlace::new();
729
730        groups.push_rect(RectToPlaceId::One, None, RectToInsert::new(60, 95, 1));
731        groups.push_rect(RectToPlaceId::Two, None, RectToInsert::new(40, 10, 1));
732        groups.push_rect(RectToPlaceId::Three, None, RectToInsert::new(60, 3, 1));
733
734        let packed = pack_rects(
735            &groups,
736            &mut targets,
737            &volume_heuristic,
738            &contains_smallest_box,
739        )
740        .unwrap();
741        let locations = packed.packed_locations;
742
743        assert_eq!(
744            locations[&RectToPlaceId::One].1,
745            PackedLocation {
746                x: 0,
747                y: 0,
748                z: 0,
749                whd: WidthHeightDepth {
750                    width: 60,
751                    height: 95,
752                    depth: 1
753                },
754                x_axis_rotation: RotatedBy::ZeroDegrees,
755                y_axis_rotation: RotatedBy::ZeroDegrees,
756                z_axis_rotation: RotatedBy::ZeroDegrees,
757            }
758        );
759        assert_eq!(
760            locations[&RectToPlaceId::Two].1,
761            PackedLocation {
762                x: 60,
763                y: 0,
764                z: 0,
765                whd: WidthHeightDepth {
766                    width: 40,
767                    height: 10,
768                    depth: 1
769                },
770                x_axis_rotation: RotatedBy::ZeroDegrees,
771                y_axis_rotation: RotatedBy::ZeroDegrees,
772                z_axis_rotation: RotatedBy::ZeroDegrees,
773            }
774        );
775        assert_eq!(
776            locations[&RectToPlaceId::Three].1,
777            PackedLocation {
778                x: 0,
779                y: 95,
780                z: 0,
781                whd: WidthHeightDepth {
782                    width: 60,
783                    height: 3,
784                    depth: 1
785                },
786                x_axis_rotation: RotatedBy::ZeroDegrees,
787                y_axis_rotation: RotatedBy::ZeroDegrees,
788                z_axis_rotation: RotatedBy::ZeroDegrees,
789            }
790        );
791    }
792
793    /// Create a handful of rectangles that need to be placed, with two of them in the same group
794    /// and the rest ungrouped.
795    /// Try placing them many times and verify that each time they are placed the exact same way.
796    #[test]
797    fn deterministic_packing() {
798        let mut previous_packed = None;
799
800        for _ in 0..5 {
801            let mut rects_to_place: GroupedRectsToPlace<&'static str, &str> =
802                GroupedRectsToPlace::new();
803
804            let mut target_bins = BTreeMap::new();
805            for bin_id in 0..5 {
806                target_bins.insert(bin_id, TargetBin::new(8, 8, 1));
807            }
808
809            let rectangles = vec![
810                "some-rectangle-0",
811                "some-rectangle-1",
812                "some-rectangle-2",
813                "some-rectangle-3",
814                "some-rectangle-4",
815            ];
816
817            for rect_id in rectangles.iter() {
818                rects_to_place.push_rect(rect_id, None, RectToInsert::new(4, 4, 1));
819            }
820
821            let packed = pack_rects(
822                &rects_to_place,
823                &mut target_bins.clone(),
824                &volume_heuristic,
825                &contains_smallest_box,
826            )
827            .unwrap();
828
829            if let Some(previous_packed) = previous_packed.as_ref() {
830                assert_eq!(&packed, previous_packed);
831            }
832
833            previous_packed = Some(packed);
834        }
835    }
836
837    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
838    enum RectToPlaceId {
839        One,
840        Two,
841        Three,
842    }
843
844    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
845    enum BinId {
846        Three,
847        Four,
848    }
849}