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;