├── .gitignore ├── test ├── mocha.opts └── test.js ├── .travis.yml ├── package.json ├── index.js ├── README.md ├── LICENSE.md └── packer.growing.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec 2 | --ui bdd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bin-pack", 3 | "version": "1.0.2", 4 | "description": "A packing algorithm for 2D bin packing. Largely based on code and a blog post by Jake Gordon.", 5 | "author": { 6 | "name": "Bryan Burgers", 7 | "email": "bryan@burgers.io", 8 | "url": "http://burgers.io" 9 | }, 10 | "main": "index.js", 11 | "keywords": [ 12 | "bin", 13 | "rectangle", 14 | "square", 15 | "sprite", 16 | "pack" 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://github.com/bryanburgers/bin-pack", 20 | "bugs": { 21 | "url": "https://github.com/bryanburgers/bin-pack/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:bryanburgers/bin-pack.git" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "mocha": "~1.12.1" 30 | }, 31 | "scripts": { 32 | "test": "node node_modules/mocha/bin/mocha" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var GrowingPacker = require('./packer.growing.js'); 4 | 5 | module.exports = function(items, options) { 6 | options = options || {}; 7 | var packer = new GrowingPacker(); 8 | var inPlace = options.inPlace || false; 9 | 10 | // Clone the items. 11 | var newItems = items.map(function(item) { return inPlace ? item : { width: item.width, height: item.height, item: item }; }); 12 | 13 | newItems = newItems.sort(function(a, b) { 14 | // TODO: check that each actually HAS a width and a height. 15 | // Sort based on the size (area) of each block. 16 | return (b.width * b.height) - (a.width * a.height); 17 | }); 18 | 19 | packer.fit(newItems); 20 | 21 | var w = newItems.reduce(function(curr, item) { return Math.max(curr, item.x + item.width); }, 0); 22 | var h = newItems.reduce(function(curr, item) { return Math.max(curr, item.y + item.height); }, 0); 23 | 24 | var ret = { 25 | width: w, 26 | height: h 27 | }; 28 | 29 | if (!inPlace) { 30 | ret.items = newItems; 31 | } 32 | 33 | return ret; 34 | }; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bin Pack 2 | 3 | [![Build Status](https://travis-ci.org/bryanburgers/bin-pack.png?branch=master)](https://travis-ci.org/bryanburgers/bin-pack) 4 | 5 | A packing algorithm for 2D bin packing. Largely based on [code][code] and a 6 | [blog post][post] by Jake Gordon. 7 | 8 | This library packs objects that have a width and a height into as small of a 9 | square as possible, using a binary tree bin packing algorithm. After packing, 10 | each object is given an (x, y) coordinate of where it would be optimally 11 | packed. 12 | 13 | The algorithm may not find the *optimal* bin packing, but it should do pretty 14 | will for things like sprite maps. 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm install bin-pack 20 | ``` 21 | 22 | ## Use 23 | 24 | ``` 25 | var pack = require('bin-pack'); 26 | var bins = [ 27 | { width: 10, height: 20 }, 28 | { width: 100, height: 100 }, 29 | { width: 50, height: 19 }, 30 | ... 31 | ]; 32 | 33 | var result = pack(bins); 34 | 35 | // result.width: width of the containing box 36 | // result.height: height of the containing box 37 | // result.items: packed items 38 | // result.items[0].x: x coordinate of the packed box 39 | // result.items[0].y: y coordinate of the packed box 40 | // result.items[0].width: width of the packed box 41 | // result.items[0].height: height of the packed box 42 | // result.items[0].item: original object that was passed in 43 | ``` 44 | 45 | If your object doesn't have `x` and `y` properties, and you don't mind a 46 | library writing to your objects, then specify `inPlace: true` and your objects 47 | will have a `x` and `y` properties added to them. 48 | 49 | ``` 50 | var pack = require('bin-pack'); 51 | var bins = [ 52 | { width: 10, height: 20 }, 53 | { width: 100, height: 100 }, 54 | { width: 50, height: 19 }, 55 | ... 56 | ]; 57 | 58 | var result = pack(bins, { inPlace: true }); 59 | // result.width: width of the containing box 60 | // result.height: height of the containing box 61 | // bins[0].x: x coordinate of the packed box 62 | // bins[0].y: y coordinate of the packed box 63 | ``` 64 | 65 | ## Contributing 66 | 67 | Contributing tests, documentation, or code is all appreciated. All code should 68 | be accompanied by valid tests. 69 | 70 | [code]: https://github.com/jakesgordon/bin-packing 71 | [post]: http://codeincomplete.com/posts/2011/5/7/bin_packing/ 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2014 Bryan Burgers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Some code based on https://github.com/jakesgordon/bin-packing. 24 | 25 | Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var pack = require('../index.js'); 4 | 5 | function intersect(item1, item2) { 6 | var item1vertices = [ 7 | { x: item1.x, y: item1.y }, // Top left 8 | { x: item1.x + item1.width, y: item1.y }, // Top right 9 | { x: item1.x + item1.width, y: item1.y + item1.height }, // Bottom right 10 | { x: item1.x, y: item1.y + item1.h } // Bottom left 11 | ]; 12 | 13 | for (var i = 0; i < item1vertices.length; i++) { 14 | var vertex = item1vertices[i]; 15 | 16 | if (item2.x < vertex.x && vertex.x < item2.x + item2.width) { 17 | if (item2.y < vertex.y && vertex.y < item2.y + item2.height) { 18 | return true; 19 | } 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | 26 | function verifyResult(result, items) { 27 | assert.ok(result != null, "Result is defined"); 28 | assert.ok(items != null, "Items is defined"); 29 | assert.ok('width' in result, "Result has a width"); 30 | assert.ok('height' in result, "Result has a height"); 31 | 32 | for (var i = 0; i < items.length; i++) { 33 | var item = items[i]; 34 | 35 | assert.ok('x' in item, "Item " + i + " has an x coordinate"); 36 | assert.ok('y' in item, "Item " + i + " has a y coordinate"); 37 | assert.ok('width' in item, "Item " + i + " has a width"); 38 | assert.ok('height' in item, "Item " + i + " has a height"); 39 | 40 | assert.ok(item.x >= 0, "Item is within the box (left)"); 41 | assert.ok(item.y >= 0, "Item is within the box (top)"); 42 | assert.ok(item.x + item.width <= result.width, "Item is within the box (right)"); 43 | assert.ok(item.y + item.height <= result.height, "Item is within the box (bottom)"); 44 | 45 | for (var j = 0; j < items.length; j++) { 46 | var otheritem = items[j]; 47 | assert.ok(!intersect(item, otheritem), "Item " + i + " does not intersect with item " + j); 48 | } 49 | } 50 | } 51 | 52 | describe('bin-pack with no options', function() { 53 | it('packs properly (basic)', function() { 54 | var bins = [ 55 | { width: 10, height: 10 }, 56 | { width: 10, height: 10 }, 57 | { width: 10, height: 10 }, 58 | { width: 10, height: 10 } 59 | ]; 60 | 61 | var result = pack(bins); 62 | assert.ok('items' in result, "Result has items"); 63 | assert.equal(result.items.length, bins.length, "Result has same amount of items as the source"); 64 | verifyResult(result, result.items); 65 | }); 66 | 67 | it('packs properly when items are passed in in the wrong order', function() { 68 | var bins = [ 69 | { width: 10, height: 10 }, 70 | { width: 100, height: 100 }, 71 | { width: 1000, height: 1000 } 72 | ]; 73 | 74 | var result = pack(bins); 75 | assert.ok('items' in result, "Result has items"); 76 | assert.equal(result.items.length, bins.length, "Result has same amount of items as the source"); 77 | verifyResult(result, result.items); 78 | }); 79 | 80 | it('packs properly when items are irregular', function() { 81 | var bins = [ 82 | { width: 10, height: 110 }, 83 | { width: 100, height: 10 }, 84 | { width: 20, height: 1 }, 85 | { width: 4, height: 48 } 86 | ]; 87 | 88 | var result = pack(bins); 89 | assert.ok('items' in result, "Result has items"); 90 | assert.equal(result.items.length, bins.length, "Result has same amount of items as the source"); 91 | verifyResult(result, result.items); 92 | }); 93 | 94 | it('does not affect original items', function() { 95 | var bins = [ 96 | { width: 10, height: 10 }, 97 | { width: 10, height: 10 }, 98 | { width: 10, height: 10 }, 99 | { width: 10, height: 10 } 100 | ]; 101 | 102 | var result = pack(bins); 103 | assert.ok(!('x' in bins[0]), "Original bin does not have x property"); 104 | assert.ok(!('y' in bins[0]), "Original bin does not have y property"); 105 | }); 106 | }); 107 | 108 | describe('bin-pack with in place option', function() { 109 | it('packs properly (basic)', function() { 110 | var bins = [ 111 | { width: 10, height: 10 }, 112 | { width: 10, height: 10 }, 113 | { width: 10, height: 10 }, 114 | { width: 10, height: 10 } 115 | ]; 116 | 117 | var result = pack(bins, { inPlace: true }); 118 | assert.ok(!('items' in result), "Result does not have items"); 119 | verifyResult(result, bins); 120 | }); 121 | 122 | it('affects original items', function() { 123 | var bins = [ 124 | { width: 10, height: 10 }, 125 | { width: 10, height: 10 }, 126 | { width: 10, height: 10 }, 127 | { width: 10, height: 10 } 128 | ]; 129 | 130 | var result = pack(bins, { inPlace: true }); 131 | assert.ok('x' in bins[0], "Original bin has an x property"); 132 | assert.ok('y' in bins[0], "Original bin has a y property"); 133 | }); 134 | 135 | it('does not sort original items', function() { 136 | var bin0 = { width: 10, height: 10 }; 137 | var bin1 = { width: 100, height: 100 }; 138 | var bin2 = { width: 1000, height: 1000 }; 139 | 140 | var bins = [ 141 | bin0, 142 | bin1, 143 | bin2 144 | ]; 145 | 146 | var result = pack(bins, { inPlace: true }); 147 | assert.ok(bins[0] === bin0, "Bin 0 is still in place 0"); 148 | assert.ok(bins[1] === bin1, "Bin 1 is still in place 1"); 149 | assert.ok(bins[2] === bin2, "Bin 2 is still in place 2"); 150 | verifyResult(result, bins); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /packer.growing.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | 3 | This is a binary tree based bin packing algorithm that is more complex than 4 | the simple Packer (packer.js). Instead of starting off with a fixed width and 5 | height, it starts with the width and height of the first block passed and then 6 | grows as necessary to accomodate each subsequent block. As it grows it attempts 7 | to maintain a roughly square ratio by making 'smart' choices about whether to 8 | grow right or down. 9 | 10 | When growing, the algorithm can only grow to the right OR down. Therefore, if 11 | the new block is BOTH wider and taller than the current target then it will be 12 | rejected. This makes it very important to initialize with a sensible starting 13 | width and height. If you are providing sorted input (largest first) then this 14 | will not be an issue. 15 | 16 | A potential way to solve this limitation would be to allow growth in BOTH 17 | directions at once, but this requires maintaining a more complex tree 18 | with 3 children (down, right and center) and that complexity can be avoided 19 | by simply chosing a sensible starting block. 20 | 21 | Best results occur when the input blocks are sorted by height, or even better 22 | when sorted by max(width,height). 23 | 24 | Inputs: 25 | ------ 26 | 27 | blocks: array of any objects that have .w and .h attributes 28 | 29 | Outputs: 30 | ------- 31 | 32 | marks each block that fits with a .fit attribute pointing to a 33 | node with .x and .y coordinates 34 | 35 | Example: 36 | ------- 37 | 38 | var blocks = [ 39 | { w: 100, h: 100 }, 40 | { w: 100, h: 100 }, 41 | { w: 80, h: 80 }, 42 | { w: 80, h: 80 }, 43 | etc 44 | etc 45 | ]; 46 | 47 | var packer = new GrowingPacker(); 48 | packer.fit(blocks); 49 | 50 | for(var n = 0 ; n < blocks.length ; n++) { 51 | var block = blocks[n]; 52 | if (block.fit) { 53 | Draw(block.fit.x, block.fit.y, block.w, block.h); 54 | } 55 | } 56 | 57 | 58 | ******************************************************************************/ 59 | 60 | var GrowingPacker = function() { }; 61 | 62 | GrowingPacker.prototype = { 63 | 64 | fit: function(blocks) { 65 | var n, node, block, len = blocks.length, fit; 66 | var width = len > 0 ? blocks[0].width : 0; 67 | var height = len > 0 ? blocks[0].height : 0; 68 | this.root = { x: 0, y: 0, width: width, height: height }; 69 | for (n = 0; n < len ; n++) { 70 | block = blocks[n]; 71 | if (node = this.findNode(this.root, block.width, block.height)) { 72 | fit = this.splitNode(node, block.width, block.height); 73 | block.x = fit.x; 74 | block.y = fit.y; 75 | } 76 | else { 77 | fit = this.growNode(block.width, block.height); 78 | block.x = fit.x; 79 | block.y = fit.y; 80 | } 81 | } 82 | }, 83 | 84 | findNode: function(root, width, height) { 85 | if (root.used) 86 | return this.findNode(root.right, width, height) || this.findNode(root.down, width, height); 87 | else if ((width <= root.width) && (height <= root.height)) 88 | return root; 89 | else 90 | return null; 91 | }, 92 | 93 | splitNode: function(node, width, height) { 94 | node.used = true; 95 | node.down = { x: node.x, y: node.y + height, width: node.width, height: node.height - height }; 96 | node.right = { x: node.x + width, y: node.y, width: node.width - width, height: height }; 97 | return node; 98 | }, 99 | 100 | growNode: function(width, height) { 101 | var canGrowDown = (width <= this.root.width); 102 | var canGrowRight = (height <= this.root.height); 103 | 104 | var shouldGrowRight = canGrowRight && (this.root.height >= (this.root.width + width )); // attempt to keep square-ish by growing right when height is much greater than width 105 | var shouldGrowDown = canGrowDown && (this.root.width >= (this.root.height + height)); // attempt to keep square-ish by growing down when width is much greater than height 106 | 107 | if (shouldGrowRight) 108 | return this.growRight(width, height); 109 | else if (shouldGrowDown) 110 | return this.growDown(width, height); 111 | else if (canGrowRight) 112 | return this.growRight(width, height); 113 | else if (canGrowDown) 114 | return this.growDown(width, height); 115 | else 116 | return null; // need to ensure sensible root starting size to avoid this happening 117 | }, 118 | 119 | growRight: function(width, height) { 120 | this.root = { 121 | used: true, 122 | x: 0, 123 | y: 0, 124 | width: this.root.width + width, 125 | height: this.root.height, 126 | down: this.root, 127 | right: { x: this.root.width, y: 0, width: width, height: this.root.height } 128 | }; 129 | var node; 130 | if (node = this.findNode(this.root, width, height)) 131 | return this.splitNode(node, width, height); 132 | else 133 | return null; 134 | }, 135 | 136 | growDown: function(width, height) { 137 | this.root = { 138 | used: true, 139 | x: 0, 140 | y: 0, 141 | width: this.root.width, 142 | height: this.root.height + height, 143 | down: { x: 0, y: this.root.height, width: this.root.width, height: height }, 144 | right: this.root 145 | }; 146 | var node; 147 | if (node = this.findNode(this.root, width, height)) 148 | return this.splitNode(node, width, height); 149 | else 150 | return null; 151 | } 152 | 153 | }; 154 | 155 | module.exports = GrowingPacker; 156 | 157 | --------------------------------------------------------------------------------