Skip to main content

binpack2d/
binpack.rs

1//! Provides a bin packing trait with associated functions to perform bin packing independently
2//! from specific bin packer algorithms.
3//!
4//! # Quick Start
5//!
6//! This example demonstrates a high-level approach for packing items into bins, without having to
7//! know about specific implementation details. Comparable code samples can be looked up in the
8//! module descriptions of the individual bin-packing algorithms.
9//!
10//! ```rust
11//! use binpack2d::{bin_new, BinType, Dimension};
12//!
13//! // Create a number of items to be placed into the bin.
14//! let items_to_place = vec![
15//!     // Items with autogenerated identifiers.
16//!     // Identifiers start at 1 and increment by 1 per call.
17//!     Dimension::new(188, 300),
18//!     Dimension::new(32, 32),
19//!     Dimension::new(420, 512),
20//!     Dimension::new(620, 384),
21//!     // Three more items with explicit identifiers: -1, 300, and 9528 respectively
22//!     Dimension::with_id(-1, 160, 214, 0),
23//!     Dimension::with_id(300, 384, 640, 0),
24//!     Dimension::with_id(9528, 400, 200, 0),
25//! ];
26//!
27//! // Create a bin with the dimensions 1024x1024, using the "MaxRects" bin type.
28//! let mut bin = bin_new(BinType::MaxRects, 1024, 1024);
29//!
30//! // Perform the bin packing operation on the list of items.
31//! let (inserted, rejected) = bin.insert_list(&items_to_place);
32//!
33//! // Let's see if our item with id=9528 was successfully inserted...
34//! if let Some(rect) = &bin.find_by_id(9528) {
35//!     println!("Item with id {} was placed into the bin at position (x: {}, y: {})",
36//!              rect.dim().id(), rect.x(), rect.y());
37//! } else {
38//!     println!("Item with id 9528 could not be placed into the bin.");
39//! }
40//!
41//! // List all successfully inserted rectangles.
42//! if !inserted.is_empty() {
43//!     inserted.iter().for_each(|rect| println!("Inserted: {}", rect));
44//! } else {
45//!     println!("No rectangles were added to the bin.");
46//! }
47//!
48//! // List all items which could not be inserted into the bin.
49//! if !rejected.is_empty() {
50//!     rejected.iter().for_each(|item| println!("Rejected: {}", item));
51//! } else {
52//!     println!("No items were rejected.");
53//! }
54//!
55//! println!("Occupancy of the bin: {:.1} %", bin.occupancy() * 100.0);
56//! ```
57
58use self::guillotine::GuillotineBin;
59use self::maxrects::MaxRectsBin;
60use crate::dimension::Dimension;
61use crate::rectangle::Rectangle;
62use std::error::Error;
63use std::fmt::{Display, Formatter};
64use std::slice::Iter;
65
66pub mod guillotine;
67pub mod maxrects;
68
69/// List of available bin packing algorithms.
70#[derive(Copy, Clone, Debug, PartialEq)]
71pub enum BinType {
72    /// Refers to the [`MaxRectsBin`] packing algorithm.
73    MaxRects,
74    /// Refers to the [`GuillotineBin`] packing algorithm.
75    Guillotine,
76}
77
78/// Represents the core of a bin-packing algorithm.
79///
80/// This trait provides a common set of methods for packing 2D rectangles into larger bins, which
81/// is implemented by all bin-packing algorithms provided by this package.
82pub trait BinPacker: Display {
83    /// Returns the width of the bin.
84    fn width(&self) -> i32;
85
86    /// Returns the height of the bin.
87    fn height(&self) -> i32;
88
89    /// Removes all mapped rectangles from the bin.
90    fn clear(&mut self) {
91        self.clear_with(4);
92    }
93
94    /// Just as [`clear`], this method removes all mapped rectangle from the bin, but
95    /// also sets initial capacity of the internal rectangle lists to improve performance.
96    ///
97    /// [`clear`]: BinPacker::clear
98    fn clear_with(&mut self, capacity: usize);
99
100    /// Increases dimension of the bin by the specified values.
101    ///
102    /// `dw` indicates how much to grow horizontally.
103    /// `dh` indicates how much to grow vertically.
104    fn grow(&mut self, dw: u32, dh: u32);
105
106    /// Attempts to shrink the bin as much as possible.
107    ///
108    /// `binary` specifies whether the shrinking process should only try to reduce dimensions
109    /// by 50 percent for each iteration.
110    ///
111    /// Does nothing if the bin is empty.
112    ///
113    /// # Examples
114    ///
115    /// ```rust
116    /// use binpack2d::{bin_new, BinType, Dimension};
117    ///
118    /// let mut bin = bin_new(BinType::MaxRects, 64, 64);
119    /// let node = Dimension::new(25, 10);
120    /// bin.insert(&node);
121    /// println!("Bin before shrinkage: {}", bin);
122    ///
123    /// bin.shrink(true);
124    /// println!("Bin after binary shrinkage: {}", bin);
125    ///
126    /// bin.shrink(false);
127    /// println!("Bin after arbitrary shrinkage: {}", bin);
128    /// ```
129    fn shrink(&mut self, binary: bool);
130
131    /// Inserts a single [`Dimension`] object into the bin.
132    ///
133    /// `dim` refers to the object to be packed into the bin.
134    ///
135    /// Returns a copy of the packed [`Rectangle`] if the object was inserted successful,
136    /// or `None` otherwise.
137    ///
138    /// **Note:** This trait method performs the operation with sane default values for
139    /// packer-specific implementations. You can override them by the bin packer's own
140    /// `set_default_*()` methods.
141    fn insert(&mut self, dim: &Dimension) -> Option<Rectangle>;
142
143    /// Attempts to insert the given list of [`Dimension`] objects into the bin.
144    ///
145    /// `nodes` specifies the list of [`Dimension`] objects to insert.
146    /// <!-- All successfully inserted objects will be removed from the list in the process. -->
147    ///
148    /// Returns a a tuple consisting of the list with all successfully inserted [`Rectangle`] objects
149    /// and a list of rejected [`Dimension`] objects.
150    ///
151    /// This method performs slower than [`insert`], but may result in more tightly
152    /// packed bins for greater numbers of dimension objects.
153    ///
154    /// [`insert`]: BinPacker::insert
155    ///
156    /// **Note:** This trait method performs the operation with sane default values for
157    /// packer-specific implementations. You can override them by the bin packer's own
158    /// `set_default_*()` methods.
159    fn insert_list(&mut self, nodes: &[Dimension]) -> (Vec<Rectangle>, Vec<Dimension>);
160
161    /// Computes the ratio of used surface area to the total bin area and returns it as a
162    /// normalized value in the range `[0.0, 1.0]`.
163    fn occupancy(&self) -> f32;
164
165    /// Extracts a slice containing the entire list of mapped rectangles.
166    ///
167    /// Equivalent to `&bin[..]`.
168    fn as_slice(&self) -> &[Rectangle];
169
170    /// Returns `true` if the list of mapped rectangles contains no entries.
171    fn is_empty(&self) -> bool;
172
173    /// Returns the number of mapped rectangles in this `Bin`.
174    fn len(&self) -> usize;
175
176    /// Returns an iterator over the list of mapped rectangles.
177    fn iter(&self) -> Iter<'_, Rectangle>;
178
179    /// Returns the first mapped rectangle with the specified identifier, if available.
180    /// Returns `None` otherwise.
181    fn find_by_id(&self, id: isize) -> Option<Rectangle>;
182
183    /// Returns a visual representation of the bin as ascii graphics `String`.
184    ///
185    /// # Notes
186    ///
187    /// Only bins with 62 or fewer mapped rectangles will be visualized. For bins with a
188    /// higher number of mapped rectangles the default string representation is returned instead.
189    ///
190    /// Digits `0` to `9` are used to visualize the first ten mapped rectangles. Ascii characters
191    /// in small letters, `a` to `z`, are used for rectangles 10 to 35. Ascii characters in capital
192    /// letters, `A` to `Z` are used for rectangles 36 to 61. Empty cells are represented by
193    /// dots (`.`)
194    fn visualize(&self) -> String;
195}
196
197/// This error is returned when items could not be placed into bins.
198#[derive(Debug, PartialEq)]
199pub enum BinError {
200    /// Item does not fit into the bin because item dimension is bigger than bin dimension.
201    ItemTooBig,
202    /// Item has a dimension of 0 and can therefore not be meaningfully placed into the bin.
203    ItemTooSmall,
204    /// A generic "catch-all" error, which is returned when the cause could not be determined.
205    Unspecified,
206}
207
208impl Display for BinError {
209    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
210        let s = match self {
211            Self::ItemTooBig => "item is too big for the bin",
212            Self::ItemTooSmall => "item with no space cannot be placed into the bin",
213            _ => "unspecified error",
214        };
215        f.write_str(s)
216    }
217}
218
219impl Error for BinError {}
220
221/// Creates an empty bin of the given size, using the specified [`BinType`] implementation.
222pub fn bin_new(bin_type: BinType, width: i32, height: i32) -> Box<dyn BinPacker> {
223    match bin_type {
224        BinType::MaxRects => Box::new(MaxRectsBin::new(width, height)),
225        BinType::Guillotine => Box::new(GuillotineBin::new(width, height)),
226    }
227}
228
229/// Creates an empty bin of the given size and reserves space for at least `capacity` number
230/// of mapped rectangle to improve performance, using the specified [`BinType`] implementation.
231pub fn bin_with_capacity(
232    bin_type: BinType,
233    width: i32,
234    height: i32,
235    capacity: usize,
236) -> Box<dyn BinPacker> {
237    match bin_type {
238        BinType::MaxRects => Box::new(MaxRectsBin::with_capacity(width, height, capacity)),
239        BinType::Guillotine => Box::new(GuillotineBin::with_capacity(width, height, capacity)),
240    }
241}
242
243/// A convenience function that attempts to insert a given list of `Dimension` objects into a
244/// variable number of bins.
245///
246/// New bins are created on demand, using the default heuristic rules for the given [`BinType`].
247///
248/// Specify true for `optimize` to use [`insert_list`] internally, which results in an improved
249/// bin layout but at the cost of a worse processing performance.
250///
251/// [`insert_list`]: BinPacker::insert_list
252///
253/// Returns a list of bins with the packed rectangle nodes as a [`Result`] value.
254///
255/// # Errors
256///
257/// A [`BinError`] is returned for nodes which are either empty or too big for the bin.
258///
259/// # Examples
260/// ```
261/// use binpack2d::{BinPacker, BinType, Dimension, pack_bins};
262///
263/// // Defining three items of different size
264/// let nodes = vec![Dimension::new(2, 4), Dimension::new(8, 6), Dimension::new(6, 6)];
265///
266/// // Returned list of bin object contains all nodes
267/// let bins = pack_bins(BinType::Guillotine, &nodes, 16, 12, true)
268///     .expect("Items should not be rejected");
269///
270/// assert_eq!(1, bins.len());
271/// assert_eq!(3, bins[0].len());
272/// ```
273pub fn pack_bins(
274    bin_type: BinType,
275    nodes: &[Dimension],
276    bin_width: i32,
277    bin_height: i32,
278    optimized: bool,
279) -> Result<Vec<Box<dyn BinPacker>>, BinError> {
280    if optimized {
281        pack_bins_list(bin_type, nodes, bin_width, bin_height)
282    } else {
283        pack_bins_single(bin_type, nodes, bin_width, bin_height)
284    }
285}
286
287/// Inserts nodes via insert_list().
288fn pack_bins_list(
289    bin_type: BinType,
290    nodes: &[Dimension],
291    bin_width: i32,
292    bin_height: i32,
293) -> Result<Vec<Box<dyn BinPacker>>, BinError> {
294    let mut bins = Vec::new();
295    if nodes.is_empty() || bin_width == 0 || bin_height == 0 {
296        return Ok(bins);
297    }
298
299    // first pass is done separately to avoid a (potentially) costly clone operation
300    let mut bin = bin_new(bin_type, bin_width, bin_height);
301    let (inserted, mut rejected) = bin.insert_list(nodes);
302
303    if inserted.is_empty() && !rejected.is_empty() {
304        // remaining nodes are too big and will be silently skipped
305        rejected.clear();
306    }
307
308    if !inserted.is_empty() {
309        bins.push(bin);
310    }
311
312    // subsequent passes are done in a loop
313    let mut nodes_left = rejected;
314    while !nodes_left.is_empty() {
315        let mut bin = bin_new(bin_type, bin_width, bin_height);
316        let (inserted, mut rejected) = bin.insert_list(&nodes_left);
317
318        if inserted.is_empty() && !rejected.is_empty() {
319            // remaining nodes are too big or too small
320            let result = rejected
321                .iter()
322                .map(|r| {
323                    if r.width_total() == 0 || r.height_total() == 0 {
324                        BinError::ItemTooSmall
325                    } else if r.width_total() > bin_width || r.height_total() > bin_height {
326                        BinError::ItemTooBig
327                    } else {
328                        BinError::Unspecified
329                    }
330                })
331                .next();
332            if let Some(result) = result {
333                return Err(result);
334            } else {
335                // Should not happen
336                eprintln!("pack_bins(): Could not insert remaining items");
337                rejected.clear();
338            }
339        }
340
341        if !inserted.is_empty() {
342            bins.push(bin);
343        }
344
345        // preparing for next iteration
346        nodes_left.clear();
347        nodes_left.append(&mut rejected);
348    }
349
350    Ok(bins)
351}
352
353/// Inserts nodes via insert().
354fn pack_bins_single(
355    bin_type: BinType,
356    nodes: &[Dimension],
357    bin_width: i32,
358    bin_height: i32,
359) -> Result<Vec<Box<dyn BinPacker>>, BinError> {
360    let mut bins = Vec::new();
361    if nodes.is_empty() || bin_width == 0 || bin_height == 0 {
362        return Ok(bins);
363    }
364
365    for node in nodes {
366        if node.is_empty() {
367            return Err(BinError::ItemTooSmall);
368        } else if node.width_total() > bin_width || node.height_total() > bin_height {
369            return Err(BinError::ItemTooBig);
370        }
371
372        // try inserting node into existing bins
373        let mut inserted = false;
374        for bin in &mut bins {
375            if bin.insert(node).is_some() {
376                inserted = true;
377                break;
378            }
379        }
380
381        // create new bin if needed
382        if !inserted {
383            bins.push(bin_new(bin_type, bin_width, bin_height));
384            if let Some(bin) = bins.last_mut() {
385                bin.insert(node).expect("Object should fit into the bin");
386            }
387        }
388    }
389
390    Ok(bins)
391}
392
393/// A helper method for visualizing bin content.
394fn visualize_bin(width: i32, height: i32, rects: &Vec<Rectangle>) -> Option<String> {
395    if width > 0 && height > 0 && rects.len() <= 62 {
396        // initializing grid
397        let size = (width * height) as usize;
398        let mut grid = vec![0u32; size];
399
400        for y in 0..height {
401            for x in 0..width {
402                for (i, r) in rects.iter().enumerate() {
403                    if r.y() <= y && r.y() + r.height() > y && r.x() <= x && r.x() + r.width() > x {
404                        let pos = (y * width + x) as usize;
405                        grid[pos] = (i + 1) as u32;
406                    }
407                }
408            }
409        }
410
411        // converting grid to string
412        let mut output = String::with_capacity(grid.len() + height as usize);
413        for y in 0..height {
414            let pos = (y * width) as usize;
415            let line: String = grid[pos..(pos + width as usize)]
416                .iter()
417                .map(|v| match v {
418                    0 => '.',
419                    1..=10 => char::from_digit(v - 1, 10).unwrap_or('?'),
420                    11..=36 => char::from_u32('a' as u32 + v - 11).unwrap_or('?'),
421                    37..=62 => char::from_u32('A' as u32 + v - 37).unwrap_or('?'),
422                    _ => '?',
423                })
424                .collect();
425            output.push_str(&line);
426            output.push('\n');
427        }
428        output.pop(); // last newline not needed
429        Some(output)
430    } else {
431        None
432    }
433}
434
435#[cfg(test)]
436mod tests;