Rectangle Packing (2D packing)

 - Tagged as

I’m working on a little thing which I don’t want to speak about just now, lets see if I can get it to do what I want first before making it public. However for that project I needed to pack several small rectangles of varying dimensions into a bigger one without them overlapping.

At first it seem like an easy task but the more I thought about it the more difficult it was. After a bit of research I found out that until today this seems to be a non resolved problem, there are algorithms which offer a pretty decent approximation to the optimal result though. The problem is commonly known as the Bin Packing problem and in this case I needed to work with two dimensional objects (rectangles) so it gets a lot more complicated.

After more googling I found an article by Jim Scott with pseudo code to solve a similar problem, the article explains on how to pack several lightmaps into a single texture. It was the easiest to understand algorithm I had found so I went ahead and implemented it in PHP. I had it in a few minutes but I wanted to fine tune the results by feeding it the rectangles by different sorting criteria, since it seems to be a good way to achieve the best results. Well, I got tired to slowly generating the image in PHP to check the results with every minor variation so I implemented a JavaScript version which allows to see the result in real time.

Here you can see the algorithm implementation or download it below.

/*
Script: RectanglePacker.js
  An algorithm implementation in JavaScript for rectangle packing.
 
Author:
  Ivan Montes <drslump@drslump.biz>, <http://blog.netxus.es>
 
License:
  LGPL - Lesser General Public License
 
Credits:
  - Algorithm based on <http://www.blackpawn.com/texts/lightmaps/default.html>
*/
 
/*
  Class: NETXUS.RectanglePacker
  A class that finds an 'efficient' position for a rectangle inside another rectangle
  without overlapping the space already taken.
 
  Algorithm based on <http://www.blackpawn.com/texts/lightmaps/default.html>
 
  It uses a binary tree to partition the space of the parent rectangle and allocate
  the passed rectangles by dividing the partitions into filled and empty.
*/
 
// Create a NETXUS namespace object if it doesn't exists
if (typeof NETXUS === 'undefined')
  var NETXUS = function() {};
 
 
/*
  Constructor: NETXUS.RectanglePacker
  Initializes the object with the given maximum dimensions
 
  Parameters:
 
    width - The containing rectangle maximum width as integer
    height - The containing rectangle maximum height as integer
 
*/
NETXUS.RectanglePacker = function ( width, height ) {
 
  this.root = {};
 
  // initialize
  this.reset( width, height );
}
 
 
/*
  Resets the object to its initial state by initializing the internal variables
 
  Parameters:
 
    width - The containing rectangle maximum width as integer
    height - The containing rectangle maximum height as integer
*/
NETXUS.RectanglePacker.prototype.reset = function ( width, height ) {
  this.root.x = 0;
  this.root.y = 0;
  this.root.w = width;
  this.root.h = height;
  delete this.root.lft;
  delete this.root.rgt;
 
  this.usedWidth = 0;
  this.usedHeight = 0;
}
 
 
/*
  Returns the actual used dimensions of the containing rectangle.
 
  Returns:
 
    A object composed of the properties: 'w' for width and 'h' for height.
*/
NETXUS.RectanglePacker.prototype.getDimensions = function () {
  return { w: this.usedWidth, h: this.usedHeight };
}
 
 
/*
  Finds a suitable place for the given rectangle
 
  Parameters:
 
    w - The rectangle width as integer.
    h - The rectangle height as integer.
 
  Returns:
 
    If there is room for the rectangle then returns the coordinates as an object
    composed of 'x' and 'y' properties.
    If it doesn't fit returns null
*/
NETXUS.RectanglePacker.prototype.findCoords = function ( w, h ) {
 
  // private function to traverse the node tree by recursion
  function recursiveFindCoords ( node, w, h ) {
 
    // private function to clone a node coords and size
    function cloneNode ( node ) {
      return {
        x: node.x,
        y: node.y,
        w: node.w,
        h: node.h
      };
    }
 
    // if we are not at a leaf then go deeper
    if ( node.lft ) {
      // check first the left branch if not found then go by the right
      var coords = recursiveFindCoords( node.lft, w, h );
      return coords ? coords : recursiveFindCoords( node.rgt, w, h );
    }
    else
    {
      // if already used or it's too big then return
      if ( node.used || w > node.w || h > node.h )
        return null;
 
      // if it fits perfectly then use this gap
      if ( w == node.w && h == node.h ) {
        node.used = true;
        return { x: node.x, y: node.y };
      }
 
      // initialize the left and right leafs by clonning the current one
      node.lft = cloneNode( node );
      node.rgt = cloneNode( node );
 
      // checks if we partition in vertical or horizontal
      if ( node.w - w > node.h - h ) {
        node.lft.w = w;
        node.rgt.x = node.x + w;
        node.rgt.w = node.w - w;
      } else {
        node.lft.h = h;
        node.rgt.y = node.y + h;
        node.rgt.h = node.h - h;
      }
 
      return recursiveFindCoords( node.lft, w, h );
    }
  }
 
  // perform the search
  var coords = recursiveFindCoords( this.root, w, h );
  // if fitted then recalculate the used dimensions
  if (coords) {
    if ( this.usedWidth < coords.x + w )
      this.usedWidth = coords.x + w;
    if ( this.usedHeight < coords.y + h )
      this.usedHeight = coords.y + h;
  }
  return coords;
}

This algorithm can be used for a wide variety of problems, from packaging small sprites in a big image to allocate tasks in complex projects (use the width for the team developers and the height for the time).

A simple usage example could be as follows:

var coords,
    packer = new NETXUS.RectanglePacker( 512, 512 );
 
for (var i=0; i<images.length; i++) {
    coords = packer.findCoords( images[i].width, images[i].height );
    alert( 'Image of ' + images[i].width + 'x' + images[i].height +
         ' allocated at ' + coords.x + 'x' + coords.y
    );
}

I’ve put up a demo so you can try it and see the results with different parameters. Tip: the best results seems to be with the blocks sorted by the magic method and reversed.

AttachmentSize
RectanglePacker.js4.27 KB