├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO ├── jest.config.js ├── package-lock.json ├── package.json ├── preview.png ├── rollup.config.js ├── src ├── abstract-bin.ts ├── geom │ └── Rectangle.ts ├── index.ts ├── maxrects-bin.ts ├── maxrects-packer.ts └── oversized-element-bin.ts ├── test ├── efficiency.spec.js ├── generictype.spec.js ├── maxrects-bin.spec.js ├── maxrects-packer.spec.js ├── oversized-element-bin.spec.js ├── rectangle.spec.js └── scenarios.json ├── tsconfig.json └── tslint.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | { 15 | "env": { 16 | "es6": true, 17 | "node": true 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "project": "./tsconfig.json", 22 | "sourceType": "module" 23 | }, 24 | "plugins": [ 25 | "eslint-plugin-import", 26 | "eslint-plugin-jsdoc", 27 | "@typescript-eslint", 28 | "@typescript-eslint/tslint" 29 | ], 30 | "root": true, 31 | "rules": { 32 | "@typescript-eslint/await-thenable": "error", 33 | "@typescript-eslint/consistent-type-assertions": "error", 34 | "@typescript-eslint/member-delimiter-style": [ 35 | "error", 36 | { 37 | "multiline": { 38 | "delimiter": "none", 39 | "requireLast": true 40 | }, 41 | "singleline": { 42 | "delimiter": "semi", 43 | "requireLast": false 44 | } 45 | } 46 | ], 47 | "@typescript-eslint/naming-convention": [ 48 | "error", 49 | { 50 | "selector": "variable", 51 | "format": [ 52 | "camelCase", 53 | "UPPER_CASE", 54 | "PascalCase" 55 | ], 56 | "leadingUnderscore": "allow", 57 | "trailingUnderscore": "forbid" 58 | } 59 | ], 60 | "@typescript-eslint/no-empty-function": "error", 61 | "@typescript-eslint/no-floating-promises": "error", 62 | "@typescript-eslint/no-misused-new": "error", 63 | "@typescript-eslint/no-unnecessary-qualifier": "error", 64 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 65 | "@typescript-eslint/no-unused-expressions": [ 66 | "error", 67 | { 68 | "allowTaggedTemplates": true, 69 | "allowShortCircuit": true 70 | } 71 | ], 72 | "@typescript-eslint/prefer-namespace-keyword": "error", 73 | "@typescript-eslint/quotes": "off", 74 | "@typescript-eslint/semi": [ 75 | "error" 76 | ], 77 | "@typescript-eslint/triple-slash-reference": [ 78 | "error", 79 | { 80 | "path": "always", 81 | "types": "prefer-import", 82 | "lib": "always" 83 | } 84 | ], 85 | "@typescript-eslint/type-annotation-spacing": "error", 86 | "@typescript-eslint/unified-signatures": "error", 87 | "brace-style": [ 88 | "error", 89 | "1tbs", 90 | { "allowSingleLine": true } 91 | ], 92 | "comma-dangle": ["error", "only-multiline"], 93 | "curly": [ 94 | "error", 95 | "multi-line" 96 | ], 97 | "eol-last": "error", 98 | "eqeqeq": [ 99 | "error", 100 | "smart" 101 | ], 102 | "id-denylist": [ 103 | "error", 104 | "any", 105 | "Number", 106 | "number", 107 | "String", 108 | "string", 109 | "Boolean", 110 | "boolean", 111 | "Undefined", 112 | "undefined" 113 | ], 114 | "id-match": "error", 115 | "import/no-deprecated": "error", 116 | "jsdoc/check-alignment": "error", 117 | "jsdoc/check-indentation": "error", 118 | "jsdoc/newline-after-description": "error", 119 | "new-parens": "error", 120 | "no-caller": "error", 121 | "no-cond-assign": "error", 122 | "no-constant-condition": "error", 123 | "no-control-regex": "error", 124 | "no-duplicate-imports": "error", 125 | "no-empty": "error", 126 | "no-empty-function": "off", 127 | "no-eval": "error", 128 | "no-fallthrough": "error", 129 | "no-invalid-regexp": "error", 130 | "no-multiple-empty-lines": "error", 131 | "no-redeclare": "error", 132 | "no-regex-spaces": "error", 133 | "no-return-await": "error", 134 | "no-throw-literal": "error", 135 | "no-trailing-spaces": "error", 136 | "no-underscore-dangle": "off", 137 | "no-unused-expressions": "off", 138 | "no-unused-labels": "error", 139 | "no-var": "error", 140 | "one-var": [ 141 | "error", 142 | "never" 143 | ], 144 | "quotes": "off", 145 | "radix": "error", 146 | "semi": "off", 147 | "space-before-function-paren": [ 148 | "error", 149 | "always" 150 | ], 151 | "space-in-parens": [ 152 | "error", 153 | "never" 154 | ], 155 | "spaced-comment": [ 156 | "error", 157 | "always", 158 | { 159 | "markers": [ 160 | "/" 161 | ] 162 | } 163 | ], 164 | "use-isnan": "error", 165 | "@typescript-eslint/tslint/config": [ 166 | "error", 167 | { 168 | "rules": { 169 | "block-spacing": [ 170 | true, 171 | "always" 172 | ], 173 | "brace-style": [ 174 | true, 175 | "1tbs", 176 | { 177 | "allowSingleLine": true 178 | } 179 | ], 180 | "handle-callback-err": [ 181 | true, 182 | "^(err|error)$" 183 | ], 184 | "no-duplicate-case": true, 185 | "no-empty-character-class": true, 186 | "no-ex-assign": true, 187 | "no-extra-boolean-cast": true, 188 | "no-inner-declarations": [ 189 | true, 190 | "functions" 191 | ], 192 | "no-multi-spaces": true, 193 | "no-unexpected-multiline": true, 194 | "object-curly-spacing": [ 195 | true, 196 | "always" 197 | ], 198 | "strict-type-predicates": true, 199 | "ter-arrow-spacing": [ 200 | true, 201 | { 202 | "before": true, 203 | "after": true 204 | } 205 | ], 206 | "ter-func-call-spacing": [ 207 | true, 208 | "never" 209 | ], 210 | "ter-indent": true, 211 | "ter-no-irregular-whitespace": true, 212 | "ter-no-sparse-arrays": true, 213 | "valid-typeof": true, 214 | "whitespace": [ 215 | true, 216 | "check-branch", 217 | "check-decl", 218 | "check-operator", 219 | "check-rest-spread", 220 | "check-type", 221 | "check-typecast", 222 | "check-type-operator", 223 | "check-preblock" 224 | ] 225 | } 226 | } 227 | ] 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | run-cover: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x, 19.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run cover 31 | - name: Coveralls Parallel 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.github_token }} 35 | path-to-lcov: ./test/coverage/lcov.info 36 | flag-name: run-${{ matrix.test_number }} 37 | parallel: true 38 | 39 | finish: 40 | needs: run-cover 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Coveralls Finished 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.github_token }} 47 | path-to-lcov: ./test/coverage/lcov.info 48 | parallel-finished: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | dist 35 | lib 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | .eslintrc 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # Typedocs 64 | docs 65 | 66 | # Mac stuff 67 | # General 68 | .DS_Store 69 | .AppleDouble 70 | .LSOverride 71 | 72 | # Icon must end with two \r 73 | Icon 74 | 75 | # Thumbnails 76 | ._* 77 | 78 | # Files that might appear in the root of a volume 79 | .DocumentRevisions-V100 80 | .fseventsd 81 | .Spotlight-V100 82 | .TemporaryItems 83 | .Trashes 84 | .VolumeIcon.icns 85 | .com.apple.timemachine.donotpresent 86 | 87 | # Directories potentially created on remote AFP share 88 | .AppleDB 89 | .AppleDesktop 90 | Network Trash Folder 91 | Temporary Items 92 | .apdisk 93 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | output 3 | node_modules 4 | .DS_Store 5 | coverage 6 | .eslintrc 7 | tslint.json 8 | .gitignore 9 | *.png 10 | .vscode 11 | docs 12 | test 13 | jest.config.js 14 | rollup.config.js 15 | .travis.yml 16 | TODO -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "stable" 5 | 6 | before_install: 7 | - npm update 8 | 9 | install: 10 | - npm install 11 | 12 | cache: 13 | directories: 14 | - "node_modules" 15 | 16 | script: 17 | - npm run cover 18 | 19 | # Send coverage data to Coveralls 20 | after_script: "cat test/coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 21 | 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Tests", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "args": [ 13 | "--runInBand", 14 | ], 15 | "preLaunchTask": "build", 16 | "internalConsoleOptions": "openOnSessionStart" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.minimap.enabled": true, 3 | "spellright.language": [ 4 | "en" 5 | ], 6 | "spellright.documentTypes": [ 7 | "markdown", 8 | "latex", 9 | "plaintext" 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build:clean", 9 | "label": "build", 10 | "problemMatcher": [ "$tsc" ], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.7.3](https://github.com/soimy/maxrects-packer/compare/v2.7.2...v2.7.3) (2022-02-02) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Recursion with non-exclusive tags addArray ([#34](https://github.com/soimy/maxrects-packer/issues/34)) ([d107163](https://github.com/soimy/maxrects-packer/commit/d107163dc214e1f4f45d1bf4241efe8e5b1b34b3)) 11 | 12 | ### [2.7.2](https://github.com/soimy/maxrects-packer/compare/v2.7.1...v2.7.2) (2021-01-15) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * addArray empty array throw error [#23](https://github.com/soimy/maxrects-packer/issues/23) ([9ba6605](https://github.com/soimy/maxrects-packer/commit/9ba6605)) 18 | 19 | 20 | 21 | ### [2.7.1](https://github.com/soimy/maxrects-packer/compare/v2.7.0...v2.7.1) (2020-09-18) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * Non-exclusive tag logic: Restart test iteration on next tag group ([8b59925](https://github.com/soimy/maxrects-packer/commit/8b59925)), closes [#20](https://github.com/soimy/maxrects-packer/issues/20) [#20](https://github.com/soimy/maxrects-packer/issues/20) 27 | 28 | 29 | 30 | ## [2.7.0](https://github.com/soimy/maxrects-packer/compare/v2.6.0...v2.7.0) (2020-09-16) 31 | 32 | 33 | ### Features 34 | 35 | * Non exclusive tag grouping ([#21](https://github.com/soimy/maxrects-packer/issues/21)) ([cf28fc5](https://github.com/soimy/maxrects-packer/commit/cf28fc5)), closes [#20](https://github.com/soimy/maxrects-packer/issues/20) 36 | 37 | 38 | 39 | ## [2.6.0](https://github.com/soimy/maxrects-packer/compare/v2.5.0...v2.6.0) (2020-02-13) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * fixes to default packing logic usage and documentation ([#12](https://github.com/soimy/maxrects-packer/issues/12)) ([caa1532](https://github.com/soimy/maxrects-packer/commit/caa1532)) 45 | 46 | 47 | ### Build System 48 | 49 | * Update rollup config & dts plugin ([1d1b80e](https://github.com/soimy/maxrects-packer/commit/1d1b80e)) 50 | 51 | 52 | ### Features 53 | 54 | * Per rectangle allowRotation ([ab68e1c](https://github.com/soimy/maxrects-packer/commit/ab68e1c)) 55 | 56 | 57 | 58 | ## [2.5.0](https://github.com/soimy/maxrects-packer/compare/v2.4.5...v2.5.0) (2019-09-03) 59 | 60 | 61 | ### Features 62 | 63 | * area or edge logic selection option ([c4dff4b](https://github.com/soimy/maxrects-packer/commit/c4dff4b)) 64 | 65 | 66 | ### Tests 67 | 68 | * efficiency tests refactoring ([0afa958](https://github.com/soimy/maxrects-packer/commit/0afa958)) 69 | 70 | 71 | 72 | ### [2.4.5](https://github.com/soimy/maxrects-packer/compare/v2.4.4...v2.4.5) (2019-08-26) 73 | 74 | 75 | 76 | ### [2.4.4](https://github.com/soimy/maxrects-packer/compare/v2.4.3...v2.4.4) (2019-07-10) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * save/load bins with tag correctly ([ac95c4a](https://github.com/soimy/maxrects-packer/commit/ac95c4a)) 82 | 83 | 84 | 85 | ### [2.4.3](https://github.com/soimy/maxrects-packer/compare/v2.4.2...v2.4.3) (2019-07-09) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * process() vertical expand not take border into account ([7382912](https://github.com/soimy/maxrects-packer/commit/7382912)) 91 | * updateBinSize() not consider rotated node ([aee9a4b](https://github.com/soimy/maxrects-packer/commit/aee9a4b)) 92 | 93 | 94 | ### Tests 95 | 96 | * add set rotation test ([0f1d301](https://github.com/soimy/maxrects-packer/commit/0f1d301)) 97 | 98 | 99 | 100 | ### [2.4.2](https://github.com/soimy/maxrects-packer/compare/v2.4.1...v2.4.2) (2019-07-08) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * handle rotated rectangle feeded to packer correctly ([81c80e0](https://github.com/soimy/maxrects-packer/commit/81c80e0)) 106 | 107 | 108 | 109 | ### [2.4.1](https://github.com/soimy/maxrects-packer/compare/v2.4.0...v2.4.1) (2019-07-04) 110 | 111 | 112 | 113 | ## [2.4.0](https://github.com/soimy/maxrects-packer/compare/v2.4.0-alpha.0...v2.4.0) (2019-06-30) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * Export IBin interface ([aaa40a4](https://github.com/soimy/maxrects-packer/commit/aaa40a4)) 119 | 120 | 121 | 122 | ## [2.4.0-alpha.0](https://github.com/soimy/maxrects-packer/compare/v2.3.0...v2.4.0-alpha.0) (2019-06-19) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * **maxrects-bin.ts:** split freerect not use rotated node ([5f06524](https://github.com/soimy/maxrects-packer/commit/5f06524)) 128 | 129 | 130 | ### Features 131 | 132 | * Add `Bin.border` to control space to edge ([62bc66b](https://github.com/soimy/maxrects-packer/commit/62bc66b)), closes [#5](https://github.com/soimy/maxrects-packer/issues/5) 133 | * Implement `dirty` status get/set of `Rectangle`&`MaxRectsBin` ([ca932ba](https://github.com/soimy/maxrects-packer/commit/ca932ba)) 134 | * Report bin dirty status ([a7527b6](https://github.com/soimy/maxrects-packer/commit/a7527b6)) 135 | * reset/repack (beta) ([eb93239](https://github.com/soimy/maxrects-packer/commit/eb93239)) 136 | 137 | 138 | 139 | ## [2.3.0](https://github.com/soimy/maxrects-packer/compare/v2.2.0...v2.3.0) (2019-06-06) 140 | 141 | 142 | ### Features 143 | 144 | * tag based group packing ([#7](https://github.com/soimy/maxrects-packer/issues/7)) ([0fa7a8c](https://github.com/soimy/maxrects-packer/commit/0fa7a8c)) 145 | 146 | 147 | 148 | ## [2.2.0](https://github.com/soimy/maxrects-packer/compare/v2.1.2...v2.2.0) (2019-06-04) 149 | 150 | 151 | ### Features 152 | 153 | * Add `.next()` method to enclose and start a new bin ([9dbe754](https://github.com/soimy/maxrects-packer/commit/9dbe754)) 154 | 155 | 156 | 157 | ### [2.1.2](https://github.com/soimy/maxrects-packer/compare/v2.1.1...v2.1.2) (2019-06-03) 158 | 159 | 160 | 161 | ### [2.1.1](https://github.com/soimy/maxrects-packer/compare/v2.1.0...v2.1.1) (2019-06-03) 162 | 163 | 164 | ### Build System 165 | 166 | * Update package config & toolchain ([b117652](https://github.com/soimy/maxrects-packer/commit/b117652)) 167 | 168 | 169 | ### Features 170 | 171 | * Add hash as 2nd sort for stable pack queue ([499b82e](https://github.com/soimy/maxrects-packer/commit/499b82e)) 172 | * Retangle class rot&data getter setter ([438014e](https://github.com/soimy/maxrects-packer/commit/438014e)) 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shen Yiming 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Max Rects Packer 2 | 3 | [![Build Status](https://travis-ci.org/soimy/maxrects-packer.svg?branch=master)](https://travis-ci.org/soimy/maxrects-packer) 4 | [![Coverage Status](https://coveralls.io/repos/github/soimy/maxrects-packer/badge.svg?branch=master)](https://coveralls.io/github/soimy/maxrects-packer?branch=master) 5 | [![npm version](https://badge.fury.io/js/maxrects-packer.svg)](https://badge.fury.io/js/maxrects-packer) 6 | ![npm](https://img.shields.io/npm/dm/maxrects-packer.svg) 7 | ![npm type definitions](https://shields-staging.herokuapp.com/npm/types/maxrects-packer.svg) 8 | 9 | A simple max rectangle 2d bin packing algorithm for packing glyphs or images into multiple sprite-sheet/atlas. Minimalist with no module dependency. 10 | 11 | This is a evolved version of [Multi-Bin-Packer](https://github.com/marekventur/multi-bin-packer) with much efficient packing algorithm. All interfaces and methods are inherited so no tweaks needed in your current code except module name. 12 | 13 | It differs from the long list of similar packages by its packing approach: Instead of creating one output bin with a minimum size this package is trying to create a minimum number of bins under a certain size. This avoids problems with single massive image files that are not browser-friendly. This can be especially useful for WebGL games where the GPU will benefit from spritesheets close to power-of-2 sizes. 14 | 15 | And you can also save/load to reuse packer to add new sprites. (Below is a demo atlas packed with two difference bitmap fonts) 16 | 17 | ![Preview image](https://raw.githubusercontent.com/soimy/maxrects-packer/master/preview.png) 18 | 19 | ## [Changelog](https://github.com/soimy/maxrects-packer/blob/master/CHANGELOG.md) 20 | 21 | ## Installing 22 | 23 | ```bash 24 | npm install maxrects-packer --save 25 | ``` 26 | 27 | ## Usage 28 | 29 | **Note:** *Since version 2.0.0 Max Rects Packer is rewritten in `typescript` and change the import method from old `require("maxrects-packer")` to `require("maxrects-packer").MaxRectsPacker` or more fashioned `import` statement.* 30 | 31 | **Note:** *Since version 2.1.0 packer can be fed with any object with `width & height` members, no need to follow `{ width: number, height: number, data: any }` pattern, if you are using `typescript`, that also mean any class extending `MaxRectsPacker.IRectangle`* 32 | 33 | **Note:** *Since version 2.1.0 Rectangle class constructor API is changed from `new Rectangle(x, y, width, height, rotated)` to `new Rectangle(width, height, x, y, rotated)`, cos most cases you only need to feed w/h and omit the rest like `new Rectangle(100, 100)` and left `x,y,rotated` to default value. 34 | 35 | ```javascript 36 | let MaxRectsPacker = require("maxrects-packer").MaxRectsPacker; 37 | const options = { 38 | smart: true, 39 | pot: true, 40 | square: false, 41 | allowRotation: true, 42 | tag: false, 43 | border: 5 44 | }; // Set packing options 45 | let packer = new MaxRectsPacker(1024, 1024, 2, options); // width, height, padding, options 46 | 47 | let input = [ // any object with width & height is OK since v2.1.0 48 | {width: 600, height: 20, name: "tree", foo: "bar"}, 49 | {width: 600, height: 20, name: "flower"}, 50 | {width: 2000, height: 2000, name: "oversized background", {frameWidth: 500, frameHeight: 500}}, 51 | {width: 1000, height: 1000, name: "background", color: 0x000000ff}, 52 | {width: 1000, height: 1000, name: "overlay", allowRotation: true} 53 | ]; 54 | 55 | packer.addArray(input); // Start packing with input array 56 | packer.next(); // Start a new packer bin 57 | packer.addArray(input.slice(2)); // Adding to the new bin 58 | packer.bins.forEach(bin => { 59 | console.log(bin.rects); 60 | }); 61 | 62 | // Reuse packer 63 | let bins = packer.save(); 64 | packer.load(bins); 65 | packer.addArray(input); 66 | 67 | ``` 68 | 69 | ## Test 70 | 71 | ```bash 72 | npm test 73 | ``` 74 | 75 | ## API 76 | 77 | Note: maxrects-packer requires node >= 4.0.0 78 | 79 | #### ```new MaxRectsPacker(maxWidth, maxHeight[, padding, options])``` 80 | 81 | Creates a new Packer. maxWidth and maxHeight are passed on to all bins. If ```padding``` is supplied all rects will be kept at least ```padding``` pixels apart. 82 | 83 | - `options.smart` packing with smallest possible size. (default is `true`) 84 | - `options.pot` bin size round up to smallest power of 2. (default is `true`) 85 | - `options.square` bin size shall alway be square. (default is `false`) 86 | - `options.allowRotation` allow 90-degree rotation while packing. (default is `false`) 87 | - `options.tag` allow tag based group packing. (default is `false`) 88 | - `options.exclusiveTag` tagged rects will have dependent bin, if set to `false`, packer will try to put tag rects into the same bin (default is `true`) 89 | - `options.border` atlas edge spacing (default is 0) 90 | - `options.logic` how to fill the rects. There are three options: 0 (max area), 1 (max edge), 2 (fillWidth). Default is 1 (max edge) 91 | 92 | #### ```packer.add(width, height, data)``` +1 overload 93 | 94 | Adds a rect to an existing bin or creates a new one to accommodate it. ```data``` can be anything, it will be stored along with the position data of each rect. 95 | 96 | #### ```packer.add({width: number, height: number, ... })``` +1 overload 97 | 98 | Adds a rect to an existing bin or creates a new one to accommodate it. Accept any object with `width & height`. If you are using `typescript`, that means any class extends `MaxRectsPacker.IRectangle` 99 | 100 | #### ```packer.addArray([{width: number, height: number, ...}, ...])``` 101 | 102 | Adds multiple rects. Since the input is automatically sorted before adding this approach usually leads to fewer bins created than separate calls to ```.add()``` 103 | 104 | #### ```packer.repack(quick: boolean = true)``` 105 | 106 | Repack all elements inside bins. If `quick == true`, only bins with `dirty` flag will be repacked. If `false` is passed, all rects inside this packer will be re-sort and repacked, might result different bin number. Slower but high packing efficiency. 107 | 108 | #### ```packer.next()``` 109 | 110 | Stop adding new element to the current bin and return a new bin. After calling `next()` all elements will no longer added to previous bins. 111 | 112 | #### ```let bins = packer.save()``` 113 | 114 | Save current bins settings and free area to an Array of objects for later use. Better to `JSON.stringify(bins)` and store in file. 115 | 116 | #### ```packer.load(bins)``` 117 | 118 | Restore previous saved `let bins = JSON.parse(fs.readFileSync(savedFile, 'utf8'));` settings and overwrite current one. Continue packing and previous packed area will not be overlapped. 119 | 120 | #### ```packer.bins``` 121 | 122 | Array of bins. Every bin has a ```width``` and ```height``` parameter as well as an array ```rects```. 123 | 124 | #### ```packer.bins[n].rects``` 125 | 126 | Array of rects for a specific bin. Every rect has ```x```, ```y```, ```width```, ```height```, ```rot``` and ```data```. In case of an rect exceeding ```maxWidth```/```maxHeight``` there will also be an ```oversized``` flag set to ```true```. 127 | 128 | ## Support for 90-degree rotation packing 129 | 130 | If `options.allowRotation` is set to `true`, packer will attempt to do an extra test in `findNode()` on rotated `Rectangle`. If the rotated one gives the best score, the given `Rectangle` will be rotated in the `Rectangle.rot` set to `true`. 131 | 132 | ## Support for tag based group packing 133 | 134 | If `options.tag` is set to `true`, packer will check if the input object has `tag: string` property, all input with same `tag` will be packed in the same bin. 135 | 136 | ## Support for oversized rectangles 137 | 138 | Normally all bins are of equal size or smaller than ```maxWidth```/```maxHeight```. If a rect is added that individually does not fit into those constraints a special bin will be created. This bin will only contain a single rect with a special "oversized" flag. This can be handled further on in the chain by displaying an error/warning or by simply ignoring it. 139 | 140 | ## Packing logic 141 | 142 | `options.logic` allows to change the method on how the algorithm selects the free spaces. There are three options: 143 | 144 | `{option.logic = 0}` Logic is MAX_AREA, selects the free space with the smallest loss of area. 145 | 146 | `{option.logic = 1}` Logic is MAX_EDGE, is default and selects the free space with the smallest loss of either width or height. 147 | 148 | `{option.logic = 2}` Logic is FILL_WIDTH, fills the complete width first before placing elements in next row. To get the used height `bin.height` only gives correct values with options: `{pot: false, square: false}`. Best results also with `option.allowRotation = true` 149 | 150 | 151 | 152 | ## Packing algorithm 153 | 154 | Use Max Rectangle Algorithm for packing, same as famous **Texture Packer** 155 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | ✔ 用Rollup一体化打包 @done(19-05-26 14:45) 2 | ✔ 测试引擎Mocha->Jest @done(19-05-26 14:45) 3 | ✔ 更新Covers 从Istanble->Jest @done(19-05-26 14:45) 4 | ✔ 更新README.md @done(19-05-26 17:34) 5 | ✔ Test增加Generic type部分 @done(19-05-26 17:04) 6 | ✔ 发布2.1.0版 @done(19-05-26 18:20) 7 | ✔ 增加Bin.border控制参数 @started(19-06-11 18:08) @done(19-06-11 18:31) @lasted(23m35s) 8 | ✔ 修正Bin.border错误 @started(19-06-11 21:56) @done(19-06-12 02:56) @lasted(5h51s) 9 | ✔ 完善border部分test @done(19-06-12 13:15) 10 | ✔ `hash`二次排序 @done(19-06-04 15:39) 11 | ☐ 完善注释和文档 12 | ✔ 发布gh-pages @done(19-06-04 15:39) 13 | ✔ 增加翻片儿`next()` @done(19-06-04 15:39) 14 | ✔ 根据tag自动分配Bin @done(19-06-06 00:32) @lasted(12s) 15 | ✔ 增加isDirty状态 @done(19-06-12 16:17) 16 | ✔ MaxRectsBin.isDirty @done(19-06-11 18:08) 17 | ✔ Rectangle.isDirty @started(19-06-12 13:15) @done(19-06-12 16:17) @lasted(3h2m6s) 18 | ✔ reset/repack功能 @started(19-06-13 14:12) @done(19-06-14 14:37) @lasted(1d25m33s) 19 | ✔ 增加test @done(19-06-14 14:37) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | // testEnvironment: 'node', 4 | testEnvironment: 'jest-environment-node', 5 | transform: {}, 6 | verbose: true, 7 | coverageDirectory: './test/coverage' 8 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maxrects-packer", 3 | "version": "2.7.3", 4 | "description": "A max rectangle 2d bin packer for packing glyphs or images into multiple sprite-sheet/atlas", 5 | "type": "module", 6 | "main": "dist/maxrects-packer.js", 7 | "module": "dist/maxrects-packer.mjs", 8 | "types": "dist/maxrects-packer.d.ts", 9 | "scripts": { 10 | "clean": "rimraf ./lib && rimraf ./dist", 11 | "build": "rollup -c", 12 | "build:clean": "npm run clean && npm run build", 13 | "doc": "typedoc && touch docs/.nojekyll", 14 | "doc:json": "typedoc --json docs/typedoc.json", 15 | "doc:publish": "gh-pages --dotfiles=true -m \"[ci skip] Updates\" -d docs", 16 | "test": "npm run build:clean && jest", 17 | "cover": "npm run build:clean && jest --coverage", 18 | "version": "standard-version", 19 | "prepare-release": "npm run test && npm run version" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://soimy@github.com/soimy/maxrects-packer.git" 24 | }, 25 | "keywords": [ 26 | "spritesheet", 27 | "atlas", 28 | "bin", 29 | "pack", 30 | "max", 31 | "rect" 32 | ], 33 | "author": "YM Shen (http://github.com/soimy)", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/soimy/maxrects-packer/issues" 37 | }, 38 | "homepage": "https://github.com/soimy/maxrects-packer#readme", 39 | "devDependencies": { 40 | "@rollup/plugin-terser": "^0.3.0", 41 | "@types/jest": "^29.2.5", 42 | "@types/node": "^18.11.18", 43 | "@typescript-eslint/eslint-plugin": "^5.47.1", 44 | "@typescript-eslint/eslint-plugin-tslint": "^5.48.0", 45 | "@typescript-eslint/parser": "^5.47.1", 46 | "ascii-table": "0.0.9", 47 | "coveralls": "^3.1.1", 48 | "cz-conventional-changelog": "^3.3.0", 49 | "esbuild": "^0.16.12", 50 | "eslint": "^8.31.0", 51 | "eslint-plugin-import": "^2.26.0", 52 | "eslint-plugin-jsdoc": "^39.6.4", 53 | "gh-pages": "^4.0.0", 54 | "jest": "^29.3.1", 55 | "rimraf": "^3.0.2", 56 | "rollup": "^3.9.1", 57 | "rollup-plugin-dts": "^5.1.0", 58 | "rollup-plugin-esbuild": "^5.0.0", 59 | "rollup-plugin-typescript2": "^0.34.1", 60 | "standard-version": "^9.5.0", 61 | "ts-jest": "^29.0.3", 62 | "ts-node": "^10.9.1", 63 | "tslib": "^2.4.1", 64 | "tslint-config-standard": "^9.0.0", 65 | "typedoc": "^0.23.23", 66 | "typescript": "^4.9.4" 67 | }, 68 | "config": { 69 | "commitizen": { 70 | "path": "cz-conventional-changelog" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soimy/maxrects-packer/1990f32547e6499ee4df5a63d7d4387921855fb0/preview.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import terser from "@rollup/plugin-terser"; 3 | 4 | const config = [ 5 | { 6 | input: "./src/index.ts", 7 | // transpiled typescript in umd and es format 8 | output: [ 9 | { file: "dist/maxrects-packer.js", name: "MaxRectsPacker", format: "umd", sourcemap: true }, 10 | { file: "dist/maxrects-packer.mjs", format: "es", sourcemap: true } 11 | ], 12 | plugins: [ typescript()] 13 | }, 14 | { 15 | input: "./src/index.ts", 16 | // uglified transpiled typescript in commonjs 17 | output: [ 18 | { file: "dist/maxrects-packer.min.js", format: "cjs", sourcemap: false } 19 | ], 20 | plugins: [ terser(), typescript() ] 21 | } 22 | ]; 23 | export default config; 24 | -------------------------------------------------------------------------------- /src/abstract-bin.ts: -------------------------------------------------------------------------------- 1 | import { IRectangle, Rectangle } from "./geom/Rectangle"; 2 | import { IOption } from "./maxrects-packer"; 3 | 4 | export interface IBin { 5 | width: number 6 | height: number 7 | maxWidth: number 8 | maxHeight: number 9 | freeRects: IRectangle[] 10 | rects: IRectangle[] 11 | options: IOption 12 | [propName: string]: any 13 | } 14 | 15 | export abstract class Bin implements IBin { 16 | public width!: number; 17 | public height!: number; 18 | public maxWidth!: number; 19 | public maxHeight!: number; 20 | public freeRects!: IRectangle[]; 21 | public rects!: T[]; 22 | public options!: IOption; 23 | public abstract add (rect: T): T | undefined; 24 | public abstract add (width: number, height: number, data: any): T | undefined; 25 | public abstract reset (deepRest: boolean): void; 26 | public abstract repack (): T[] | undefined; 27 | 28 | public data?: any; 29 | public tag?: string; 30 | 31 | protected _dirty: number = 0; 32 | get dirty (): boolean { return this._dirty > 0 || this.rects.some(rect => rect.dirty); } 33 | /** 34 | * Set bin dirty status 35 | * 36 | * @memberof Bin 37 | */ 38 | public setDirty (value: boolean = true): void { 39 | this._dirty = value ? this._dirty + 1 : 0; 40 | if (!value) { 41 | for (let rect of this.rects) { 42 | if (rect.setDirty) rect.setDirty(false); 43 | } 44 | } 45 | } 46 | 47 | public abstract clone (): Bin; 48 | } 49 | -------------------------------------------------------------------------------- /src/geom/Rectangle.ts: -------------------------------------------------------------------------------- 1 | export interface IRectangle { 2 | width: number 3 | height: number 4 | x: number 5 | y: number 6 | [propName: string]: any 7 | } 8 | 9 | export class Rectangle implements IRectangle { 10 | /** 11 | * Oversized tag on rectangle which is bigger than packer itself. 12 | * 13 | * @type {boolean} 14 | * @memberof Rectangle 15 | */ 16 | public oversized: boolean = false; 17 | 18 | /** 19 | * Creates an instance of Rectangle. 20 | * 21 | * @param {number} [width=0] 22 | * @param {number} [height=0] 23 | * @param {number} [x=0] 24 | * @param {number} [y=0] 25 | * @param {boolean} [rot=false] 26 | * @param {boolean} [allowRotation=false] 27 | * @memberof Rectangle 28 | */ 29 | constructor ( 30 | width: number = 0, 31 | height: number = 0, 32 | x: number = 0, 33 | y: number = 0, 34 | rot: boolean = false, 35 | allowRotation: boolean | undefined = undefined 36 | ) { 37 | this._width = width; 38 | this._height = height; 39 | this._x = x; 40 | this._y = y; 41 | this._data = {}; 42 | this._rot = rot; 43 | this._allowRotation = allowRotation; 44 | } 45 | 46 | /** 47 | * Test if two given rectangle collide each other 48 | * 49 | * @static 50 | * @param {IRectangle} first 51 | * @param {IRectangle} second 52 | * @returns 53 | * @memberof Rectangle 54 | */ 55 | public static Collide (first: IRectangle, second: IRectangle) { return first.collide(second); } 56 | 57 | /** 58 | * Test if the first rectangle contains the second one 59 | * 60 | * @static 61 | * @param {IRectangle} first 62 | * @param {IRectangle} second 63 | * @returns 64 | * @memberof Rectangle 65 | */ 66 | public static Contain (first: IRectangle, second: IRectangle) { return first.contain(second); } 67 | 68 | /** 69 | * Get the area (w * h) of the rectangle 70 | * 71 | * @returns {number} 72 | * @memberof Rectangle 73 | */ 74 | public area (): number { return this.width * this.height; } 75 | 76 | /** 77 | * Test if the given rectangle collide with this rectangle. 78 | * 79 | * @param {IRectangle} rect 80 | * @returns {boolean} 81 | * @memberof Rectangle 82 | */ 83 | public collide (rect: IRectangle): boolean { 84 | return ( 85 | rect.x < this.x + this.width && 86 | rect.x + rect.width > this.x && 87 | rect.y < this.y + this.height && 88 | rect.y + rect.height > this.y 89 | ); 90 | } 91 | 92 | /** 93 | * Test if this rectangle contains the given rectangle. 94 | * 95 | * @param {IRectangle} rect 96 | * @returns {boolean} 97 | * @memberof Rectangle 98 | */ 99 | public contain (rect: IRectangle): boolean { 100 | return (rect.x >= this.x && rect.y >= this.y && 101 | rect.x + rect.width <= this.x + this.width && rect.y + rect.height <= this.y + this.height); 102 | } 103 | 104 | protected _width: number; 105 | get width (): number { return this._width; } 106 | set width (value: number) { 107 | if (value === this._width) return; 108 | this._width = value; 109 | this._dirty ++; 110 | } 111 | 112 | protected _height: number; 113 | get height (): number { return this._height; } 114 | set height (value: number) { 115 | if (value === this._height) return; 116 | this._height = value; 117 | this._dirty ++; 118 | } 119 | 120 | protected _x: number; 121 | get x (): number { return this._x; } 122 | set x (value: number) { 123 | if (value === this._x) return; 124 | this._x = value; 125 | this._dirty ++; 126 | } 127 | 128 | protected _y: number; 129 | get y (): number { return this._y; } 130 | set y (value: number) { 131 | if (value === this._y) return; 132 | this._y = value; 133 | this._dirty ++; 134 | } 135 | 136 | protected _rot: boolean = false; 137 | 138 | /** 139 | * If the rectangle is rotated 140 | * 141 | * @type {boolean} 142 | * @memberof Rectangle 143 | */ 144 | get rot (): boolean { return this._rot; } 145 | 146 | /** 147 | * Set the rotate tag of the rectangle. 148 | * 149 | * note: after `rot` is set, `width/height` of this rectangle is swaped. 150 | * 151 | * @memberof Rectangle 152 | */ 153 | set rot (value: boolean) { 154 | if (this._allowRotation === false) return; 155 | 156 | if (this._rot !== value) { 157 | const tmp = this.width; 158 | this.width = this.height; 159 | this.height = tmp; 160 | this._rot = value; 161 | this._dirty ++; 162 | } 163 | } 164 | 165 | protected _allowRotation: boolean | undefined = undefined; 166 | 167 | /** 168 | * If the rectangle allow rotation 169 | * 170 | * @type {boolean} 171 | * @memberof Rectangle 172 | */ 173 | get allowRotation (): boolean | undefined { return this._allowRotation; } 174 | 175 | /** 176 | * Set the allowRotation tag of the rectangle. 177 | * 178 | * @memberof Rectangle 179 | */ 180 | set allowRotation (value: boolean | undefined) { 181 | if (this._allowRotation !== value) { 182 | this._allowRotation = value; 183 | this._dirty ++; 184 | } 185 | } 186 | 187 | protected _data: any; 188 | get data (): any { return this._data; } 189 | set data (value: any) { 190 | if (value === null || value === this._data) return; 191 | this._data = value; 192 | // extract allowRotation settings 193 | if (typeof value === "object" && value.hasOwnProperty("allowRotation")) { 194 | this._allowRotation = value.allowRotation; 195 | } 196 | this._dirty ++; 197 | } 198 | 199 | protected _dirty: number = 0; 200 | get dirty (): boolean { return this._dirty > 0; } 201 | public setDirty (value: boolean = true): void { this._dirty = value ? this._dirty + 1 : 0; } 202 | } 203 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Rectangle, IRectangle } from "./geom/Rectangle"; 2 | export { MaxRectsPacker, PACKING_LOGIC, IOption } from "./maxrects-packer"; 3 | export { Bin, IBin } from "./abstract-bin"; 4 | export { MaxRectsBin } from "./maxrects-bin"; 5 | export { OversizedElementBin } from "./oversized-element-bin"; 6 | -------------------------------------------------------------------------------- /src/maxrects-bin.ts: -------------------------------------------------------------------------------- 1 | import { EDGE_MAX_VALUE, PACKING_LOGIC, IOption } from "./maxrects-packer"; 2 | import { Rectangle, IRectangle } from "./geom/Rectangle"; 3 | import { Bin } from "./abstract-bin"; 4 | 5 | export class MaxRectsBin extends Bin { 6 | public width: number; 7 | public height: number; 8 | public freeRects: Rectangle[] = []; 9 | public rects: T[] = []; 10 | private verticalExpand: boolean = false; 11 | private stage: Rectangle; 12 | private border: number; 13 | public options: IOption = { 14 | smart: true, 15 | pot: true, 16 | square: true, 17 | allowRotation: false, 18 | tag: false, 19 | exclusiveTag: true, 20 | border: 0, 21 | logic: PACKING_LOGIC.MAX_EDGE 22 | }; 23 | 24 | constructor ( 25 | public maxWidth: number = EDGE_MAX_VALUE, 26 | public maxHeight: number = EDGE_MAX_VALUE, 27 | public padding: number = 0, 28 | options: IOption = {} 29 | ) { 30 | super(); 31 | this.options = { ...this.options, ...options }; 32 | this.width = this.options.smart ? 0 : maxWidth; 33 | this.height = this.options.smart ? 0 : maxHeight; 34 | this.border = this.options.border ? this.options.border : 0; 35 | this.freeRects.push(new Rectangle( 36 | this.maxWidth + this.padding - this.border * 2, 37 | this.maxHeight + this.padding - this.border * 2, 38 | this.border, 39 | this.border)); 40 | this.stage = new Rectangle(this.width, this.height); 41 | } 42 | 43 | public add (rect: T): T | undefined; 44 | public add (width: number, height: number, data: any): T | undefined; 45 | public add (...args: any[]): any { 46 | let data: any; 47 | let rect: IRectangle; 48 | if (args.length === 1) { 49 | if (typeof args[0] !== 'object') throw new Error("MacrectsBin.add(): Wrong parameters"); 50 | rect = args[0] as T; 51 | // Check if rect.tag match bin.tag, if bin.tag not defined, it will accept any rect 52 | let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; 53 | if (this.options.tag && this.options.exclusiveTag && this.tag !== tag) return undefined; 54 | } else { 55 | data = args.length > 2 ? args[2] : null; 56 | // Check if data.tag match bin.tag, if bin.tag not defined, it will accept any rect 57 | if (this.options.tag && this.options.exclusiveTag) { 58 | if (data && this.tag !== data.tag) return undefined; 59 | if (!data && this.tag) return undefined; 60 | } 61 | rect = new Rectangle(args[0], args[1]); 62 | rect.data = data; 63 | rect.setDirty(false); 64 | } 65 | 66 | const result = this.place(rect); 67 | if (result) this.rects.push(result); 68 | return result; 69 | } 70 | 71 | public repack (): T[] | undefined { 72 | let unpacked: T[] = []; 73 | this.reset(); 74 | // re-sort rects from big to small 75 | this.rects.sort((a, b) => { 76 | const result = Math.max(b.width, b.height) - Math.max(a.width, a.height); 77 | if (result === 0 && a.hash && b.hash) { 78 | return a.hash > b.hash ? -1 : 1; 79 | } else return result; 80 | }); 81 | for (let rect of this.rects) { 82 | if (!this.place(rect)) { 83 | unpacked.push(rect); 84 | } 85 | } 86 | for (let rect of unpacked) this.rects.splice(this.rects.indexOf(rect), 1); 87 | return unpacked.length > 0 ? unpacked : undefined; 88 | } 89 | 90 | public reset (deepReset: boolean = false, resetOption: boolean = false): void { 91 | if (deepReset) { 92 | if (this.data) delete this.data; 93 | if (this.tag) delete this.tag; 94 | this.rects = []; 95 | if (resetOption) { 96 | this.options = { 97 | smart: true, 98 | pot: true, 99 | square: true, 100 | allowRotation: false, 101 | tag: false, 102 | border: 0 103 | }; 104 | } 105 | } 106 | this.width = this.options.smart ? 0 : this.maxWidth; 107 | this.height = this.options.smart ? 0 : this.maxHeight; 108 | this.border = this.options.border ? this.options.border : 0; 109 | this.freeRects = [new Rectangle( 110 | this.maxWidth + this.padding - this.border * 2, 111 | this.maxHeight + this.padding - this.border * 2, 112 | this.border, 113 | this.border)]; 114 | this.stage = new Rectangle(this.width, this.height); 115 | this._dirty = 0; 116 | } 117 | 118 | public clone (): MaxRectsBin { 119 | let clonedBin: MaxRectsBin = new MaxRectsBin(this.maxWidth, this.maxHeight, this.padding, this.options); 120 | for (let rect of this.rects) { 121 | clonedBin.add(rect); 122 | } 123 | return clonedBin; 124 | } 125 | 126 | private place (rect: IRectangle): T | undefined { 127 | // recheck if tag matched 128 | let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; 129 | if (this.options.tag && this.options.exclusiveTag && this.tag !== tag) return undefined; 130 | 131 | let node: IRectangle | undefined; 132 | let allowRotation: boolean | undefined; 133 | // getter/setter do not support hasOwnProperty() 134 | if (rect.hasOwnProperty("_allowRotation") && rect.allowRotation !== undefined) { 135 | allowRotation = rect.allowRotation; // Per Rectangle allowRotation override packer settings 136 | } else { 137 | allowRotation = this.options.allowRotation; 138 | } 139 | node = this.findNode(rect.width + this.padding, rect.height + this.padding, allowRotation); 140 | 141 | if (node) { 142 | this.updateBinSize(node); 143 | let numRectToProcess = this.freeRects.length; 144 | let i: number = 0; 145 | while (i < numRectToProcess) { 146 | if (this.splitNode(this.freeRects[i], node)) { 147 | this.freeRects.splice(i, 1); 148 | numRectToProcess--; 149 | i--; 150 | } 151 | i++; 152 | } 153 | this.pruneFreeList(); 154 | this.verticalExpand = this.options.logic === PACKING_LOGIC.FILL_WIDTH ? false : this.width > this.height ? true : false; 155 | rect.x = node.x; 156 | rect.y = node.y; 157 | if (rect.rot === undefined) rect.rot = false; 158 | rect.rot = node.rot ? !rect.rot : rect.rot; 159 | this._dirty ++; 160 | return rect as T; 161 | } else if (!this.verticalExpand) { 162 | if (this.updateBinSize(new Rectangle( 163 | rect.width + this.padding, rect.height + this.padding, 164 | this.width + this.padding - this.border, this.border 165 | )) || this.updateBinSize(new Rectangle( 166 | rect.width + this.padding, rect.height + this.padding, 167 | this.border, this.height + this.padding - this.border 168 | ))) { 169 | return this.place(rect); 170 | } 171 | } else { 172 | if (this.updateBinSize(new Rectangle( 173 | rect.width + this.padding, rect.height + this.padding, 174 | this.border, this.height + this.padding - this.border 175 | )) || this.updateBinSize(new Rectangle( 176 | rect.width + this.padding, rect.height + this.padding, 177 | this.width + this.padding - this.border, this.border 178 | ))) { 179 | return this.place(rect); 180 | } 181 | } 182 | return undefined; 183 | } 184 | 185 | /** 186 | * Find the best rect out of freeRects 187 | * This method has different logics to resolve the best rect. 188 | * @param width 189 | * @param height 190 | * @param allowRotation 191 | * @returns Rectangle of the best rect for placement 192 | */ 193 | private findNode (width: number, height: number, allowRotation?: boolean): Rectangle | undefined { 194 | // scoring based on one single number. The smaller the better the choice. 195 | let score: number = Number.MAX_VALUE; 196 | let areaFit: number; 197 | let r: Rectangle; 198 | let bestNode: Rectangle | undefined; 199 | for (let i in this.freeRects) { 200 | r = this.freeRects[i]; 201 | if (r.width >= width && r.height >= height) { 202 | if (this.options.logic === PACKING_LOGIC.MAX_AREA) { 203 | // the rect with the lowest rest area wins 204 | areaFit = r.width * r.height - width * height; 205 | } else if (this.options.logic === PACKING_LOGIC.FILL_WIDTH) { 206 | // this logic needs to factors to build a score. 207 | // 1. rect position: choose the most rightest one with the lowest y coordinate. 208 | // 2. size that needs to grow to place the element. The lower the better score (leads to optimal rotation placement) 209 | 210 | const currentRectPositionScore = r.x + r.y * this.maxWidth; // each y value adds a full width to the score to balance x and y coordinates to a single number 211 | const numberOfBetterRects = this.freeRects.filter(rect => (rect.x + rect.y * this.maxWidth) < currentRectPositionScore).length; // search if there are rects, righter and a lower y coordinate. 212 | 213 | // calculate how much space will be add to total height 214 | const heightToGain = r.y + height - this.height; 215 | 216 | areaFit = numberOfBetterRects + heightToGain; // add both factors together 217 | } else { 218 | // the rect with the lowest loss of either width or height wins 219 | areaFit = Math.min(r.width - width, r.height - height); 220 | } 221 | if (areaFit < score) { 222 | bestNode = new Rectangle(width, height, r.x, r.y); 223 | score = areaFit; 224 | } 225 | } 226 | 227 | if (!allowRotation) continue; 228 | 229 | // Continue to test 90-degree rotated rectangle 230 | if (r.width >= height && r.height >= width) { 231 | if (this.options.logic === PACKING_LOGIC.MAX_AREA) { 232 | areaFit = r.width * r.height - height * width; 233 | } else if (this.options.logic === PACKING_LOGIC.FILL_WIDTH) { 234 | const currentRectPositionScore = r.x + r.y * this.maxWidth; 235 | const numberOfBetterRects = this.freeRects.filter(rect => (rect.x + rect.y * this.maxWidth) < currentRectPositionScore).length; // search if there are rects, righter and a lower y coordinate. 236 | 237 | // calculate how much space will be add to total height 238 | const heightToGain = r.y + width - this.height; 239 | 240 | areaFit = numberOfBetterRects + heightToGain; // add both factors together 241 | } else { 242 | areaFit = Math.min(r.height - width, r.width - height); 243 | } 244 | if (areaFit < score) { 245 | bestNode = new Rectangle(height, width, r.x, r.y, true); // Rotated node 246 | score = areaFit; 247 | } 248 | } 249 | } 250 | return bestNode; 251 | } 252 | 253 | private splitNode (freeRect: IRectangle, usedNode: IRectangle): boolean { 254 | // Test if usedNode intersect with freeRect 255 | if (!freeRect.collide(usedNode)) return false; 256 | 257 | // Do vertical split 258 | if (usedNode.x < freeRect.x + freeRect.width && usedNode.x + usedNode.width > freeRect.x) { 259 | // New node at the top side of the used node 260 | if (usedNode.y > freeRect.y && usedNode.y < freeRect.y + freeRect.height) { 261 | let newNode: Rectangle = new Rectangle(freeRect.width, usedNode.y - freeRect.y, freeRect.x, freeRect.y); 262 | this.freeRects.push(newNode); 263 | } 264 | // New node at the bottom side of the used node 265 | if (usedNode.y + usedNode.height < freeRect.y + freeRect.height) { 266 | let newNode = new Rectangle( 267 | freeRect.width, 268 | freeRect.y + freeRect.height - (usedNode.y + usedNode.height), 269 | freeRect.x, 270 | usedNode.y + usedNode.height 271 | ); 272 | this.freeRects.push(newNode); 273 | } 274 | } 275 | 276 | // Do Horizontal split 277 | if (usedNode.y < freeRect.y + freeRect.height && 278 | usedNode.y + usedNode.height > freeRect.y) { 279 | // New node at the left side of the used node. 280 | if (usedNode.x > freeRect.x && usedNode.x < freeRect.x + freeRect.width) { 281 | let newNode = new Rectangle(usedNode.x - freeRect.x, freeRect.height, freeRect.x, freeRect.y); 282 | this.freeRects.push(newNode); 283 | } 284 | // New node at the right side of the used node. 285 | if (usedNode.x + usedNode.width < freeRect.x + freeRect.width) { 286 | let newNode = new Rectangle( 287 | freeRect.x + freeRect.width - (usedNode.x + usedNode.width), 288 | freeRect.height, 289 | usedNode.x + usedNode.width, 290 | freeRect.y 291 | ); 292 | this.freeRects.push(newNode); 293 | } 294 | } 295 | return true; 296 | } 297 | 298 | private pruneFreeList () { 299 | // Go through each pair of freeRects and remove any rects that is redundant 300 | let i: number = 0; 301 | let j: number = 0; 302 | let len: number = this.freeRects.length; 303 | while (i < len) { 304 | j = i + 1; 305 | let tmpRect1 = this.freeRects[i]; 306 | while (j < len) { 307 | let tmpRect2 = this.freeRects[j]; 308 | if (tmpRect2.contain(tmpRect1)) { 309 | this.freeRects.splice(i, 1); 310 | i--; 311 | len--; 312 | break; 313 | } 314 | if (tmpRect1.contain(tmpRect2)) { 315 | this.freeRects.splice(j, 1); 316 | j--; 317 | len--; 318 | } 319 | j++; 320 | } 321 | i++; 322 | } 323 | } 324 | 325 | private updateBinSize (node: IRectangle): boolean { 326 | if (!this.options.smart) return false; 327 | if (this.stage.contain(node)) return false; 328 | let tmpWidth: number = Math.max(this.width, node.x + node.width - this.padding + this.border); 329 | let tmpHeight: number = Math.max(this.height, node.y + node.height - this.padding + this.border); 330 | if (this.options.allowRotation) { 331 | // do extra test on rotated node whether it's a better choice 332 | const rotWidth: number = Math.max(this.width, node.x + node.height - this.padding + this.border); 333 | const rotHeight: number = Math.max(this.height, node.y + node.width - this.padding + this.border); 334 | if (rotWidth * rotHeight < tmpWidth * tmpHeight) { 335 | tmpWidth = rotWidth; 336 | tmpHeight = rotHeight; 337 | } 338 | } 339 | if (this.options.pot) { 340 | tmpWidth = Math.pow(2, Math.ceil(Math.log(tmpWidth) * Math.LOG2E)); 341 | tmpHeight = Math.pow(2, Math.ceil(Math.log(tmpHeight) * Math.LOG2E)); 342 | } 343 | if (this.options.square) { 344 | tmpWidth = tmpHeight = Math.max(tmpWidth, tmpHeight); 345 | } 346 | if (tmpWidth > this.maxWidth + this.padding || tmpHeight > this.maxHeight + this.padding) { 347 | return false; 348 | } 349 | this.expandFreeRects(tmpWidth + this.padding, tmpHeight + this.padding); 350 | this.width = this.stage.width = tmpWidth; 351 | this.height = this.stage.height = tmpHeight; 352 | return true; 353 | } 354 | 355 | private expandFreeRects (width: number, height: number) { 356 | this.freeRects.forEach((freeRect, index) => { 357 | if (freeRect.x + freeRect.width >= Math.min(this.width + this.padding - this.border, width)) { 358 | freeRect.width = width - freeRect.x - this.border; 359 | } 360 | if (freeRect.y + freeRect.height >= Math.min(this.height + this.padding - this.border, height)) { 361 | freeRect.height = height - freeRect.y - this.border; 362 | } 363 | }, this); 364 | this.freeRects.push(new Rectangle( 365 | width - this.width - this.padding, 366 | height - this.border * 2, 367 | this.width + this.padding - this.border, 368 | this.border)); 369 | this.freeRects.push(new Rectangle( 370 | width - this.border * 2, 371 | height - this.height - this.padding, 372 | this.border, 373 | this.height + this.padding - this.border)); 374 | this.freeRects = this.freeRects.filter(freeRect => { 375 | return !(freeRect.width <= 0 || freeRect.height <= 0 || freeRect.x < this.border || freeRect.y < this.border); 376 | }); 377 | this.pruneFreeList(); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/maxrects-packer.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle, IRectangle } from "./geom/Rectangle"; 2 | import { MaxRectsBin } from "./maxrects-bin"; 3 | import { OversizedElementBin } from "./oversized-element-bin"; 4 | import { Bin, IBin } from "./abstract-bin"; 5 | 6 | export const EDGE_MAX_VALUE: number = 4096; 7 | export const EDGE_MIN_VALUE: number = 128; 8 | export enum PACKING_LOGIC { 9 | MAX_AREA = 0, 10 | MAX_EDGE = 1, 11 | FILL_WIDTH = 2, 12 | } 13 | 14 | /** 15 | * Options for MaxRect Packer 16 | * 17 | * @property {boolean} options.smart Smart sizing packer (default is true) 18 | * @property {boolean} options.pot use power of 2 sizing (default is true) 19 | * @property {boolean} options.square use square size (default is false) 20 | * @property {boolean} options.allowRotation allow rotation packing (default is false) 21 | * @property {boolean} options.tag allow auto grouping based on `rect.tag` (default is false) 22 | * @property {boolean} options.exclusiveTag tagged rects will have dependent bin, if set to `false`, packer will try to put tag rects into the same bin (default is true) 23 | * @property {boolean} options.border atlas edge spacing (default is 0) 24 | * @property {PACKING_LOGIC} options.logic MAX_AREA or MAX_EDGE based sorting logic (default is MAX_EDGE) 25 | * @export 26 | * @interface Option 27 | */ 28 | export interface IOption { 29 | smart?: boolean 30 | pot?: boolean 31 | square?: boolean 32 | allowRotation?: boolean 33 | tag?: boolean 34 | exclusiveTag?: boolean 35 | border?: number 36 | logic?: PACKING_LOGIC 37 | } 38 | 39 | export class MaxRectsPacker { 40 | 41 | /** 42 | * The Bin array added to the packer 43 | * 44 | * @type {Bin[]} 45 | * @memberof MaxRectsPacker 46 | */ 47 | public bins: Bin[]; 48 | 49 | /** 50 | * Options for MaxRect Packer 51 | * 52 | * @property {boolean} options.smart Smart sizing packer (default is true) 53 | * @property {boolean} options.pot use power of 2 sizing (default is true) 54 | * @property {boolean} options.square use square size (default is false) 55 | * @property {boolean} options.allowRotation allow rotation packing (default is false) 56 | * @property {boolean} options.tag allow auto grouping based on `rect.tag` (default is false) 57 | * @property {boolean} options.exclusiveTag tagged rects will have dependent bin, if set to `false`, packer will try to put tag rects into the same bin (default is true) 58 | * @property {boolean} options.border atlas edge spacing (default is 0) 59 | * @property {PACKING_LOGIC} options.logic MAX_AREA or MAX_EDGE based sorting logic (default is MAX_EDGE) 60 | * @export 61 | * @interface Option 62 | */ 63 | public options: IOption = { 64 | smart: true, 65 | pot: true, 66 | square: false, 67 | allowRotation: false, 68 | tag: false, 69 | exclusiveTag: true, 70 | border: 0, 71 | logic: PACKING_LOGIC.MAX_EDGE 72 | }; 73 | 74 | /** 75 | * Creates an instance of MaxRectsPacker. 76 | * 77 | * @param {number} width of the output atlas (default is 4096) 78 | * @param {number} height of the output atlas (default is 4096) 79 | * @param {number} padding between glyphs/images (default is 0) 80 | * @param {IOption} [options={}] (Optional) packing options 81 | * @memberof MaxRectsPacker 82 | */ 83 | constructor ( 84 | public width: number = EDGE_MAX_VALUE, 85 | public height: number = EDGE_MAX_VALUE, 86 | public padding: number = 0, 87 | options: IOption = {} 88 | ) { 89 | this.bins = []; 90 | this.options = { ...this.options, ...options }; 91 | } 92 | 93 | /** 94 | * Add a bin/rectangle object with data to packer 95 | * 96 | * @param {number} width of the input bin/rectangle 97 | * @param {number} height of the input bin/rectangle 98 | * @param {*} data custom data object 99 | * @memberof MaxRectsPacker 100 | */ 101 | public add (width: number, height: number, data: any): T; 102 | /** 103 | * Add a bin/rectangle object extends IRectangle to packer 104 | * 105 | * @template T Generic type extends IRectangle interface 106 | * @param {T} rect the rect object add to the packer bin 107 | * @memberof MaxRectsPacker 108 | */ 109 | public add (rect: T): T; 110 | public add (...args: any[]): any { 111 | if (args.length === 1) { 112 | if (typeof args[0] !== 'object') throw new Error("MacrectsPacker.add(): Wrong parameters"); 113 | const rect = args[0] as T; 114 | if (!((rect.width <= this.width && rect.height <= this.height) || (this.options.allowRotation && rect.width <= this.height && rect.height <= this.width))) { 115 | this.bins.push(new OversizedElementBin(rect)); 116 | } else { 117 | let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect) !== undefined); 118 | if (!added) { 119 | let bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); 120 | let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; 121 | if (this.options.tag && tag) bin.tag = tag; 122 | bin.add(rect); 123 | this.bins.push(bin); 124 | } 125 | } 126 | return rect; 127 | } else { 128 | const rect: IRectangle = new Rectangle(args[0], args[1]); 129 | if (args.length > 2) rect.data = args[2]; 130 | 131 | if (!((rect.width <= this.width && rect.height <= this.height) || (this.options.allowRotation && rect.width <= this.height && rect.height <= this.width))) { 132 | this.bins.push(new OversizedElementBin(rect as T)); 133 | } else { 134 | let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect as T) !== undefined); 135 | if (!added) { 136 | let bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); 137 | if (this.options.tag && rect.data.tag) bin.tag = rect.data.tag; 138 | bin.add(rect as T); 139 | this.bins.push(bin); 140 | } 141 | } 142 | return rect as T; 143 | } 144 | } 145 | 146 | /** 147 | * Add an Array of bins/rectangles to the packer. 148 | * 149 | * `Javascript`: Any object has property: { width, height, ... } is accepted. 150 | * 151 | * `Typescript`: object shall extends `MaxrectsPacker.IRectangle`. 152 | * 153 | * note: object has `hash` property will have more stable packing result 154 | * 155 | * @param {IRectangle[]} rects Array of bin/rectangles 156 | * @memberof MaxRectsPacker 157 | */ 158 | public addArray (rects: T[]) { 159 | if (!this.options.tag || this.options.exclusiveTag) { 160 | // if not using tag or using exclusiveTag, old approach 161 | this.sort(rects, this.options.logic).forEach(rect => this.add(rect)); 162 | } else { 163 | // sort rects by tags first 164 | if (rects.length === 0) return; 165 | rects.sort((a,b) => { 166 | const aTag = (a.data && a.data.tag) ? a.data.tag : a.tag ? a.tag : undefined; 167 | const bTag = (b.data && b.data.tag) ? b.data.tag : b.tag ? b.tag : undefined; 168 | return bTag === undefined ? -1 : aTag === undefined ? 1 : bTag > aTag ? -1 : 1; 169 | }); 170 | 171 | // iterate all bins to find the first bin which can place rects with same tag 172 | // 173 | let currentTag: any; 174 | let currentIdx: number = 0; 175 | let targetBin = this.bins.slice(this._currentBinIndex).find((bin, binIndex) => { 176 | let testBin = bin.clone(); 177 | for (let i = currentIdx; i < rects.length; i++) { 178 | const rect = rects[i]; 179 | const tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; 180 | 181 | // initialize currentTag 182 | if (i === 0) currentTag = tag; 183 | 184 | if (tag !== currentTag) { 185 | // all current tag memeber tested successfully 186 | currentTag = tag; 187 | // do addArray() 188 | this.sort(rects.slice(currentIdx, i), this.options.logic).forEach(r => bin.add(r)); 189 | currentIdx = i; 190 | 191 | // recrusively addArray() with remaining rects 192 | this.addArray(rects.slice(i)); 193 | return true; 194 | } 195 | 196 | // remaining untagged rect will use normal addArray() 197 | if (tag === undefined) { 198 | // do addArray() 199 | this.sort(rects.slice(i), this.options.logic).forEach(r => this.add(r)); 200 | currentIdx = rects.length; 201 | // end test 202 | return true; 203 | } 204 | 205 | // still in the same tag group 206 | if (testBin.add(rect) === undefined) { 207 | // add the rects that could fit into the bins already 208 | // do addArray() 209 | this.sort(rects.slice(currentIdx, i), this.options.logic).forEach(r => bin.add(r)); 210 | currentIdx = i; 211 | 212 | // current bin cannot contain all tag members 213 | // procceed to test next bin 214 | return false; 215 | } 216 | } 217 | 218 | // all rects tested 219 | // do addArray() to the remaining tag group 220 | this.sort(rects.slice(currentIdx), this.options.logic).forEach(r => bin.add(r)); 221 | return true; 222 | }); 223 | 224 | // create a new bin if no current bin fit 225 | if (!targetBin) { 226 | const rect = rects[currentIdx]; 227 | const bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); 228 | const tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; 229 | if (this.options.tag && this.options.exclusiveTag && tag) bin.tag = tag; 230 | this.bins.push(bin); 231 | // Add the rect to the newly created bin 232 | bin.add(rect); 233 | currentIdx++; 234 | this.addArray(rects.slice(currentIdx)); 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Reset entire packer to initial states, keep settings 241 | * 242 | * @memberof MaxRectsPacker 243 | */ 244 | public reset (): void { 245 | this.bins = []; 246 | this._currentBinIndex = 0; 247 | } 248 | 249 | /** 250 | * Repack all elements inside bins 251 | * 252 | * @param {boolean} [quick=true] quick repack only dirty bins 253 | * @returns {void} 254 | * @memberof MaxRectsPacker 255 | */ 256 | public repack (quick: boolean = true): void { 257 | if (quick) { 258 | let unpack: T[] = []; 259 | for (let bin of this.bins) { 260 | if (bin.dirty) { 261 | let up = bin.repack(); 262 | if (up) unpack.push(...up); 263 | } 264 | } 265 | this.addArray(unpack); 266 | return; 267 | } 268 | if (!this.dirty) return; 269 | const allRects = this.rects; 270 | this.reset(); 271 | this.addArray(allRects); 272 | } 273 | 274 | /** 275 | * Stop adding new element to the current bin and return a new bin. 276 | * 277 | * note: After calling `next()` all elements will no longer added to previous bins. 278 | * 279 | * @returns {Bin} 280 | * @memberof MaxRectsPacker 281 | */ 282 | public next (): number { 283 | this._currentBinIndex = this.bins.length; 284 | return this._currentBinIndex; 285 | } 286 | 287 | /** 288 | * Load bins to the packer, overwrite exist bins 289 | * 290 | * @param {MaxRectsBin[]} bins MaxRectsBin objects 291 | * @memberof MaxRectsPacker 292 | */ 293 | public load (bins: IBin[]) { 294 | bins.forEach((bin, index) => { 295 | if (bin.maxWidth > this.width || bin.maxHeight > this.height) { 296 | this.bins.push(new OversizedElementBin(bin.width, bin.height, {})); 297 | } else { 298 | let newBin = new MaxRectsBin(this.width, this.height, this.padding, bin.options); 299 | newBin.freeRects.splice(0); 300 | bin.freeRects.forEach((r, i) => { 301 | newBin.freeRects.push(new Rectangle(r.width, r.height, r.x, r.y)); 302 | }); 303 | newBin.width = bin.width; 304 | newBin.height = bin.height; 305 | if (bin.tag) newBin.tag = bin.tag; 306 | this.bins[index] = newBin; 307 | } 308 | }, this); 309 | } 310 | 311 | /** 312 | * Output current bins to save 313 | * 314 | * @memberof MaxRectsPacker 315 | */ 316 | public save (): IBin[] { 317 | let saveBins: IBin[] = []; 318 | this.bins.forEach((bin => { 319 | let saveBin: IBin = { 320 | width: bin.width, 321 | height: bin.height, 322 | maxWidth: bin.maxWidth, 323 | maxHeight: bin.maxHeight, 324 | freeRects: [], 325 | rects: [], 326 | options: bin.options 327 | }; 328 | if (bin.tag) saveBin = { ...saveBin, tag: bin.tag }; 329 | bin.freeRects.forEach(r => { 330 | saveBin.freeRects.push({ 331 | x: r.x, 332 | y: r.y, 333 | width: r.width, 334 | height: r.height 335 | }); 336 | }); 337 | saveBins.push(saveBin); 338 | })); 339 | return saveBins; 340 | } 341 | 342 | /** 343 | * Sort the given rects based on longest edge or surface area. 344 | * 345 | * If rects have the same sort value, will sort by second key `hash` if presented. 346 | * 347 | * @private 348 | * @param {T[]} rects 349 | * @param {PACKING_LOGIC} [logic=PACKING_LOGIC.MAX_EDGE] sorting logic, "area" or "edge" 350 | * @returns 351 | * @memberof MaxRectsPacker 352 | */ 353 | private sort (rects: T[], logic: IOption['logic'] = PACKING_LOGIC.MAX_EDGE) { 354 | return rects.slice().sort((a, b) => { 355 | const result = (logic === PACKING_LOGIC.MAX_EDGE) ? 356 | Math.max(b.width, b.height) - Math.max(a.width, a.height) : 357 | b.width * b.height - a.width * a.height; 358 | if (result === 0 && a.hash && b.hash) { 359 | return a.hash > b.hash ? -1 : 1; 360 | } else return result; 361 | }); 362 | } 363 | 364 | private _currentBinIndex: number = 0; 365 | /** 366 | * Return current functioning bin index, perior to this wont accept any new elements 367 | * 368 | * @readonly 369 | * @type {number} 370 | * @memberof MaxRectsPacker 371 | */ 372 | get currentBinIndex (): number { return this._currentBinIndex; } 373 | 374 | /** 375 | * Returns dirty status of all child bins 376 | * 377 | * @readonly 378 | * @type {boolean} 379 | * @memberof MaxRectsPacker 380 | */ 381 | get dirty (): boolean { return this.bins.some(bin => bin.dirty); } 382 | 383 | /** 384 | * Return all rectangles in this packer 385 | * 386 | * @readonly 387 | * @type {T[]} 388 | * @memberof MaxRectsPacker 389 | */ 390 | get rects (): T[] { 391 | let allRects: T[] = []; 392 | for (let bin of this.bins) { 393 | allRects.push(...bin.rects); 394 | } 395 | return allRects; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/oversized-element-bin.ts: -------------------------------------------------------------------------------- 1 | import { IRectangle, Rectangle } from "./geom/Rectangle"; 2 | import { IOption } from "./maxrects-packer"; 3 | import { Bin } from "./abstract-bin"; 4 | 5 | export class OversizedElementBin extends Bin { 6 | public width: number; 7 | public height: number; 8 | public data: any; 9 | public maxWidth: number; 10 | public maxHeight: number; 11 | public options: IOption; 12 | public rects: T[] = []; 13 | public freeRects: IRectangle[]; 14 | 15 | constructor (rect: T); 16 | constructor (width: number, height: number, data: any); 17 | constructor (...args: any[]) { 18 | super(); 19 | if (args.length === 1) { 20 | if (typeof args[0] !== 'object') throw new Error("OversizedElementBin: Wrong parameters"); 21 | const rect = args[0]; 22 | this.rects = [rect]; 23 | this.width = rect.width; 24 | this.height = rect.height; 25 | this.data = rect.data; 26 | rect.oversized = true; 27 | } else { 28 | this.width = args[0]; 29 | this.height = args[1]; 30 | this.data = args.length > 2 ? args[2] : null; 31 | const rect: IRectangle = new Rectangle(this.width, this.height); 32 | rect.oversized = true; 33 | rect.data = this.data; 34 | this.rects.push(rect as T); 35 | } 36 | this.freeRects = []; 37 | this.maxWidth = this.width; 38 | this.maxHeight = this.height; 39 | this.options = { smart: false, pot: false, square: false }; 40 | } 41 | 42 | add () { return undefined; } 43 | reset (deepReset: boolean = false): void { 44 | // nothing to do here 45 | } 46 | repack (): T[] | undefined { return undefined; } 47 | clone(): Bin { 48 | let clonedBin: OversizedElementBin = new OversizedElementBin(this.rects[0]); 49 | return clonedBin; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/efficiency.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let MaxRectsPacker = require("../dist/maxrects-packer").MaxRectsPacker; 4 | let PACKING_LOGIC = require("../dist/maxrects-packer").PACKING_LOGIC; 5 | let AsciiTable = require("ascii-table"); 6 | 7 | const SCENARIOS = require("./scenarios.json"); 8 | 9 | let rectSizeSum = SCENARIOS.map(scenario => { 10 | return scenario.reduce((memo, rect) => memo + rect.width * rect.height, 0); 11 | }); 12 | 13 | describe('Efficiency', () => { 14 | const AREA_CANDIDATES = [ 15 | {name: "1024x2048:0", factory: () => new MaxRectsPacker(1024, 2048, 0, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_AREA })}, 16 | {name: "1024x2048:1", factory: () => new MaxRectsPacker(1024, 2048, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_AREA })}, 17 | {name: "1024x2048:1:Rot", factory: () => new MaxRectsPacker(1024, 2048, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_AREA })}, 18 | {name: "1024x1024:0", factory: () => new MaxRectsPacker(1024, 1024, 0, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_AREA })}, 19 | {name: "1024x1024:1", factory: () => new MaxRectsPacker(1024, 1024, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_AREA })}, 20 | {name: "1024x1024:1:Rot", factory: () => new MaxRectsPacker(1024, 1024, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_AREA })}, 21 | {name: "2048:2048:1", factory: () => new MaxRectsPacker(2048, 2048, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_AREA })}, 22 | {name: "2048:2048:1:Rot", factory: () => new MaxRectsPacker(2048, 2048, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_AREA })} 23 | ]; 24 | 25 | const EDGE_CANDIDATES = [ 26 | {name: "1024x2048:0", factory: () => new MaxRectsPacker(1024, 2048, 0, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_EDGE })}, 27 | {name: "1024x2048:1", factory: () => new MaxRectsPacker(1024, 2048, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_EDGE })}, 28 | {name: "1024x2048:1:Rot", factory: () => new MaxRectsPacker(1024, 2048, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_EDGE })}, 29 | {name: "1024x1024:0", factory: () => new MaxRectsPacker(1024, 1024, 0, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_EDGE })}, 30 | {name: "1024x1024:1", factory: () => new MaxRectsPacker(1024, 1024, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_EDGE })}, 31 | {name: "1024x1024:1:Rot", factory: () => new MaxRectsPacker(1024, 1024, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_EDGE })}, 32 | {name: "2048:2048:1", factory: () => new MaxRectsPacker(2048, 2048, 1, { smart: true, pot: true, square: false, logic: PACKING_LOGIC.MAX_EDGE })}, 33 | {name: "2048:2048:1:Rot", factory: () => new MaxRectsPacker(2048, 2048, 1, { smart: true, pot: true, square: true, allowRotation: true, logic: PACKING_LOGIC.MAX_EDGE })} 34 | ]; 35 | 36 | test.skip('area logic', () => { 37 | let heading = ["#", "size"].concat(AREA_CANDIDATES.map(c => c.name)); 38 | let results = AREA_CANDIDATES.map(candidate => meassureEfficiency(candidate.factory)); 39 | let rows = createRows(results); 40 | 41 | console.log(new AsciiTable({ heading, rows }).toString()); 42 | }); 43 | 44 | test.skip('edge logic', () => { 45 | let heading = ["#", "size"].concat(EDGE_CANDIDATES.map(c => c.name)); 46 | let results = EDGE_CANDIDATES.map(candidate => meassureEfficiency(candidate.factory)); 47 | let rows = createRows(results); 48 | 49 | console.log(new AsciiTable({ heading, rows }).toString()); 50 | }); 51 | 52 | test('combined best of', () => { 53 | let heading = ["#", "size"].concat(AREA_CANDIDATES.map(c => c.name)); 54 | let results1 = EDGE_CANDIDATES.map(candidate => meassureEfficiency(candidate.factory)); 55 | let results2 = AREA_CANDIDATES.map(candidate => meassureEfficiency(candidate.factory)); 56 | let results = results1.map((scenario, scenarioIndex) => scenario.map((result1, resultIndex) => { 57 | const result2 = results2[scenarioIndex][resultIndex]; 58 | if (result1.bins < result2.bins) { 59 | result1.method = "E"; 60 | return result1; 61 | } else if (result1.bins > result2.bins) { 62 | result2.method = "A"; 63 | return result2; 64 | } else if (result1.efficieny > result2.efficieny) { 65 | result1.method = "E"; 66 | return result1; 67 | } else if (result1.efficieny < result2.efficieny) { 68 | result2.method = "A"; 69 | return result2; 70 | } else { 71 | result1.method = ""; 72 | return result1; 73 | } 74 | })); 75 | let rows = createRows(results); 76 | 77 | console.log(new AsciiTable({ heading, rows }).toString()); 78 | }); 79 | }); 80 | 81 | function meassureEfficiency (factory) { 82 | return SCENARIOS.map((scenario, i) => { 83 | let packer = factory(); 84 | packer.addArray(scenario); 85 | 86 | let bins = packer.bins.length; 87 | let rectSize = rectSizeSum[i]; 88 | let usedSize = packer.bins.reduce((memo, bin) => memo + bin.width * bin.height, 0); 89 | let efficieny = rectSize / usedSize; 90 | return {bins, rectSize, usedSize, efficieny}; 91 | }); 92 | } 93 | 94 | function toPercent (input) { 95 | return Math.round(input * 1000) / 10 + "%"; 96 | } 97 | 98 | function createRows (results) { 99 | return SCENARIOS.map((scenario, i) => { 100 | return [i, rectSizeSum[i]].concat(results.map(resultCandidate => { 101 | let result = resultCandidate[i]; 102 | return `${toPercent(result.efficieny)} (${result.bins} bins)${result.method}`; 103 | })); 104 | }).concat([["sum", ""].concat(results.map(result => { 105 | let usedSize = result.reduce((memo, data) => memo + data.usedSize, 0); 106 | let rectSize = result.reduce((memo, data) => memo + data.rectSize, 0); 107 | let totalBins = result.reduce((memo, data) => memo + data.bins, 0); 108 | return `${toPercent(rectSize / usedSize)} (${totalBins} bins)`; 109 | }))]); 110 | } 111 | -------------------------------------------------------------------------------- /test/generictype.spec.js: -------------------------------------------------------------------------------- 1 | let MaxRectsPacker = require('../dist/maxrects-packer').MaxRectsPacker; 2 | let Rectangle = require('../dist/maxrects-packer').Rectangle; 3 | 4 | class Block extends Rectangle { 5 | constructor (width = 0, height = 0, x = 0, y = 0, rot = false) { 6 | super(); 7 | this.color = 0xffffffff; // Extended attribution 8 | } 9 | getColor () { // Extended method 10 | return this.color; 11 | } 12 | } 13 | 14 | let packer; 15 | beforeEach(() => { 16 | packer = new MaxRectsPacker(1024, 1024, 0); 17 | }); 18 | 19 | test('generic type extends Rectangle class', () => { 20 | let blocks = []; 21 | blocks.push(new Block(512, 512)); 22 | blocks.push(new Block(512, 512)); 23 | blocks.push(new Block(512, 512)); 24 | let colors = [0x000000ff, 0xff0000ff]; 25 | colors.forEach((color, i) => { 26 | blocks[i].color = color; 27 | }); 28 | 29 | packer.addArray(blocks); 30 | expect(packer.bins.length).toBe(1); 31 | expect(packer.bins[0].rects.length).toBe(blocks.length); 32 | expect(packer.bins[0].rects[0].x).toBe(0); 33 | expect(packer.bins[0].rects[0].y).toBe(0); 34 | expect(packer.bins[0].rects[0].color).toBe(colors[0]); 35 | expect(packer.bins[0].rects[0].getColor()).toBe(colors[0]); 36 | expect(packer.bins[0].rects[2].color).toBe(0xffffffff); 37 | }); 38 | 39 | test('anonymous class with width, height', () => { 40 | let blocks = []; 41 | blocks.push({width: 512, height: 512}); 42 | blocks.push({width: 512, height: 512}); 43 | blocks.push({width: 512, height: 512}); 44 | let colors = [0x000000ff, 0xff0000ff]; 45 | colors.forEach((color, i) => { 46 | blocks[i].color = color; 47 | }); 48 | 49 | packer.addArray(blocks); 50 | expect(packer.bins.length).toBe(1); 51 | expect(packer.bins[0].rects.length).toBe(blocks.length); 52 | expect(packer.bins[0].rects[0].x).toBe(0); 53 | expect(packer.bins[0].rects[0].y).toBe(0); 54 | expect(packer.bins[0].rects[0].color).toBe(colors[0]); 55 | expect(packer.bins[0].rects[2].color).toBeUndefined(); 56 | }); 57 | -------------------------------------------------------------------------------- /test/maxrects-bin.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | "use strict"; 3 | 4 | let MaxRectsBin = require("../dist/maxrects-packer").MaxRectsBin; 5 | let Rectangle = require("../dist/maxrects-packer").Rectangle; 6 | 7 | const EDGE_MAX_VALUE = 4096; 8 | const EDGE_MIN_VALUE = 128; 9 | const opt = { 10 | smart: true, 11 | pot: true, 12 | square: false, 13 | allowRotation: false, 14 | tag: true, 15 | 16 | }; 17 | 18 | let bin; 19 | 20 | describe("no padding", () => { 21 | beforeEach(() => { 22 | bin = new MaxRectsBin(1024, 1024, 0, opt); 23 | }); 24 | 25 | test("is initially empty", () => { 26 | expect(bin.width).toBe(0); 27 | expect(bin.height).toBe(0); 28 | }); 29 | 30 | test("adds rects correctly", () => { 31 | let position = bin.add(200, 100, {}); 32 | expect(position.x).toBe(0); 33 | expect(position.y).toBe(0); 34 | }); 35 | 36 | test("report/set bin dirty status", () => { 37 | bin.add(200, 100, {}); 38 | expect(bin.dirty).toBe(true); // add element to bin will render bin dirty 39 | bin.setDirty(false); 40 | expect(bin.dirty).toBe(false); // clean bin dirty 41 | bin.add(200, 100, {}); 42 | expect(bin.dirty).toBe(true); // add new element is dirty 43 | bin.setDirty(false); 44 | bin.setDirty(); 45 | expect(bin.dirty).toBe(true); // setDirty is dirty 46 | bin.reset(); 47 | expect(bin.dirty).toBe(false); // reset clean dirty 48 | let rect = bin.add(new Rectangle(200, 100)); 49 | bin.setDirty(false); 50 | rect.width = 256; 51 | expect(bin.dirty).toBe(true); // modify rects is dirty 52 | }); 53 | 54 | 55 | test("updates size correctly", () => { 56 | bin.add(200, 100, {}); 57 | expect(bin.width).toBe(256); 58 | expect(bin.height).toBe(128); 59 | }); 60 | 61 | test("stores data correctly", () => { 62 | bin.add(200, 100, {foo: "bar"}); 63 | expect(bin.rects[0].data.foo).toBe("bar"); 64 | }); 65 | 66 | test("set rotation correctly", () => { 67 | bin = new MaxRectsBin(1024, 1024, 0, {...opt, allowRotation: true}); 68 | bin.add({width: 512, height: 1024}); 69 | bin.add({width: 1024, height: 512}); 70 | expect(bin.rects.length).toBe(2); 71 | expect(bin.rects[1].rot).toBe(true); 72 | bin.reset(true); 73 | bin.add({width: 512, height: 1024}); 74 | bin.add({width: 1024, height: 512, rot: true}); 75 | expect(bin.rects.length).toBe(2); 76 | expect(bin.rects[1].rot).toBe(false); 77 | }); 78 | 79 | test("stores custom rect correctly", () => { 80 | bin.add({width: 200, height: 100, foo: "bar"}); 81 | expect(bin.rects[0].foo).toBe("bar"); 82 | }); 83 | 84 | test("none tag bin reject all tagged rects on exclusive tag mode", () => { 85 | bin.add({width: 200, height: 100}); 86 | bin.add({width: 200, height: 100, tag: "foo"}); 87 | bin.add({width: 200, height: 100, tag: "bar"}); 88 | expect(bin.rects.length).toBe(1); 89 | }); 90 | 91 | test("tagged bin reject different tagged rects on exclusive tag mode", () => { 92 | bin.tag = "foo"; 93 | let one = bin.add({width: 200, height: 100, tag: "foo"}); 94 | let two = bin.add({width: 200, height: 100, tag: "bar"}); 95 | expect(bin.rects.length).toBe(1); 96 | expect(bin.rects[0].tag).toBe("foo"); 97 | expect(two).toBeUndefined(); 98 | }); 99 | 100 | test("tagged bin accept different tagged rects on non-exclusive tag mode", () => { 101 | bin.tag = "foo"; 102 | bin.options.exclusiveTag = false; 103 | let one = bin.add({width: 200, height: 100, tag: "foo"}); 104 | let two = bin.add({width: 200, height: 100, tag: "bar"}); 105 | expect(bin.rects.length).toBe(2); 106 | expect(bin.rects[0].tag).toBe("foo"); 107 | expect(two).toBeDefined(); 108 | }); 109 | 110 | test("fits squares correctly", () => { 111 | let i = 0; 112 | while(bin.add(100, 100, {num: i})) { 113 | // circuit breaker 114 | if (i++ === 1000) { 115 | break; 116 | } 117 | } 118 | expect(i).toBe(100); 119 | expect(bin.rects.length).toBe(100); 120 | expect(bin.width).toBe(1024); 121 | expect(bin.height).toBe(1024); 122 | 123 | bin.rects.forEach((rect, i) => { 124 | expect(rect.data.num).toBe(i); 125 | }); 126 | }); 127 | 128 | test("reset & deep reset", () => { 129 | bin.add({width: 200, height: 100}); 130 | bin.add({width: 200, height: 100}); 131 | bin.add({width: 200, height: 100}); 132 | expect(bin.rects.length).toBe(3); 133 | expect(bin.width).toBe(512); 134 | bin.reset(); 135 | expect(bin.width).toBe(0); 136 | expect(bin.freeRects.length).toBe(1); 137 | let unpacked = bin.repack(); 138 | expect(unpacked).toBeUndefined(); 139 | expect(bin.width).toBe(512); 140 | bin.reset(true); 141 | expect(bin.width).toBe(0); 142 | expect(bin.rects.length).toBe(0); 143 | expect(bin.options.tag).toBe(true); 144 | bin.reset(true, true); 145 | expect(bin.options.tag).toBe(false); 146 | }); 147 | 148 | test("repack", () => { 149 | let rect1 = bin.add({width: 512, height: 512, id: "one"}); 150 | let rect2 = bin.add({width: 512, height: 512, id: "two"}); 151 | let rect3 = bin.add({width: 512, height: 512, id: "three"}); 152 | rect2.width = 1024; 153 | rect2.height = 513; 154 | let unpacked = bin.repack(); 155 | expect(unpacked.length).toBe(2); 156 | expect(unpacked[0].id).toBe("one"); 157 | expect(unpacked[1].id).toBe("three"); 158 | expect(bin.rects.length).toBe(1); 159 | }); 160 | 161 | test("monkey testing", () => { 162 | let rects = []; 163 | while (true) { 164 | let width = Math.floor(Math.random() * 200); 165 | let height = Math.floor(Math.random() * 200); 166 | let rect = new Rectangle(width, height); 167 | 168 | let position = bin.add(rect); 169 | if (position) { 170 | expect(position.width).toBe(width); 171 | expect(position.height).toBe(height); 172 | rects.push(position); 173 | } else { 174 | break; 175 | } 176 | } 177 | 178 | expect(bin.width).toBeLessThanOrEqual(1024); 179 | expect(bin.height).toBeLessThanOrEqual(1024); 180 | 181 | rects.forEach(rect1 => { 182 | // Make sure rects are not overlapping 183 | rects.forEach(rect2 => { 184 | if (rect1 !== rect2) { 185 | expect(rect1.collide(rect2)).toBe(false, "intersection detected: " + JSON.stringify(rect1) + " " + JSON.stringify(rect2)); 186 | } 187 | }); 188 | 189 | // Make sure no rect is outside bounds 190 | expect(rect1.x + rect1.width).toBeLessThanOrEqual(bin.width); 191 | expect(rect1.y + rect1.height).toBeLessThanOrEqual(bin.height); 192 | }); 193 | }); 194 | }); 195 | 196 | let padding = 4; 197 | 198 | describe("padding", () => { 199 | beforeEach(() => { 200 | bin = new MaxRectsBin(1024, 1024, padding, opt); 201 | }); 202 | 203 | test("is initially empty", () => { 204 | expect(bin.width).toBe(0); 205 | expect(bin.height).toBe(0); 206 | }); 207 | 208 | test("handles padding correctly", () => { 209 | bin.add(512, 512, {}); 210 | bin.add(512 - padding, 512, {}); 211 | bin.add(512, 512 - padding, {}); 212 | expect(bin.width).toBe(1024); 213 | expect(bin.height).toBe(1024); 214 | expect(bin.rects.length).toBe(3); 215 | }); 216 | 217 | test("adds rects with sizes close to the max", () => { 218 | expect(bin.add(1024, 1024)).toBeDefined(); 219 | expect(bin.rects.length).toBe(1); 220 | }); 221 | 222 | test("monkey testing", () => { 223 | // bin = new MaxRectsBin(1024, 1024, 40); 224 | let rects = []; 225 | while (true) { 226 | let width = Math.floor(Math.random() * 200); 227 | let height = Math.floor(Math.random() * 200); 228 | let rect = new Rectangle(width, height); 229 | 230 | let position = bin.add(rect); 231 | if (position) { 232 | expect(position.width).toBe(width); 233 | expect(position.height).toBe(height); 234 | rects.push(position); 235 | } else { 236 | break; 237 | } 238 | } 239 | 240 | expect(bin.width).toBeLessThanOrEqual(1024); 241 | expect(bin.height).toBeLessThanOrEqual(1024); 242 | 243 | rects.forEach(rect1 => { 244 | // Make sure rects are not overlapping 245 | rects.forEach(rect2 => { 246 | if (rect1 !== rect2) { 247 | try { 248 | expect(rect1.collide(rect2)).toBe(false); 249 | } catch (e) { 250 | throw new Error("intersection detected: " + JSON.stringify(rect1) + " " + JSON.stringify(rect2)); 251 | } 252 | } 253 | }); 254 | 255 | // Make sure no rect is outside bounds 256 | expect(rect1.x).toBeGreaterThanOrEqual(0); 257 | expect(rect1.y).toBeGreaterThanOrEqual(0); 258 | expect(rect1.x + rect1.width).toBeLessThanOrEqual(bin.width); 259 | expect(rect1.y + rect1.height).toBeLessThanOrEqual(bin.height); 260 | }); 261 | }); 262 | }); 263 | 264 | padding = 4; 265 | let border = 5; 266 | 267 | describe("border", () => { 268 | beforeEach(() => { 269 | const borderOpt = {...opt, ...{border: border, square: false}}; 270 | bin = new MaxRectsBin(1024, 1024, padding, borderOpt); 271 | }); 272 | 273 | test("is initially empty", () => { 274 | expect(bin.width).toBe(0); 275 | expect(bin.height).toBe(0); 276 | }); 277 | 278 | test("handles border & padding correctly", () => { 279 | let size = 512 - border * 2; // 280 | let pos1 = bin.add(size + 1, size, {}); 281 | expect(pos1.x).toBe(5); 282 | expect(pos1.y).toBe(5); 283 | expect(bin.width).toBe(1024); 284 | expect(bin.height).toBe(512); 285 | let pos2 = bin.add(size, size, {}); 286 | expect(pos2.x - pos1.x - pos1.width).toBe(padding); // handle space correctly 287 | expect(pos2.y).toBe(border); 288 | expect(bin.width).toBe(1024); 289 | expect(bin.height).toBe(512); 290 | bin.add(size, size, {}); 291 | bin.add(512, 508, {}); 292 | expect(bin.width).toBe(1024); 293 | expect(bin.height).toBe(1024); 294 | expect(bin.rects.length).toBe(3); 295 | }); 296 | 297 | test("adds rects with sizes close to the max", () => { 298 | expect(bin.add(1024, 1024)).toBeUndefined(); 299 | expect(bin.rects.length).toBe(0); 300 | }); 301 | 302 | let repeat = 5; 303 | test(`super monkey testing (${repeat} loop)`, () => { 304 | while (repeat > 0) { 305 | padding = Math.floor(Math.random() * 10); 306 | border = Math.floor(Math.random() * 20); 307 | const borderOpt = {...opt, ...{border: border, square: false}}; 308 | bin = new MaxRectsBin(1024, 1024, padding, borderOpt); 309 | 310 | let rects = []; 311 | while (true) { 312 | let width = Math.floor(Math.random() * 200); 313 | let height = Math.floor(Math.random() * 200); 314 | let rect = new Rectangle(width, height); 315 | 316 | let position = bin.add(rect); 317 | if (position) { 318 | expect(position.width).toBe(width); 319 | expect(position.height).toBe(height); 320 | rects.push(position); 321 | } else { 322 | break; 323 | } 324 | } 325 | 326 | expect(bin.width).toBeLessThanOrEqual(1024); 327 | expect(bin.height).toBeLessThanOrEqual(1024); 328 | 329 | rects.forEach(rect1 => { 330 | // Make sure rects are not overlapping 331 | rects.forEach(rect2 => { 332 | if (rect1 !== rect2) { 333 | try { 334 | expect(rect1.collide(rect2)).toBe(false); 335 | } catch (e) { 336 | throw new Error("intersection detected: " + JSON.stringify(rect1) + " " + JSON.stringify(rect2)); 337 | } 338 | } 339 | }); 340 | 341 | // Make sure no rect is outside bounds 342 | expect(rect1.x).toBeGreaterThanOrEqual(bin.options.border); 343 | expect(rect1.y).toBeGreaterThanOrEqual(bin.options.border); 344 | expect(rect1.x + rect1.width).toBeLessThanOrEqual(bin.width - bin.options.border); 345 | expect(rect1.y + rect1.height).toBeLessThanOrEqual(bin.height - bin.options.border); 346 | }); 347 | repeat --; 348 | } 349 | }); 350 | }); 351 | 352 | describe("logic FILL_WIDTH", () => { 353 | beforeEach(() => { 354 | bin = new MaxRectsBin(1024, 512, 0, {allowRotation: true, logic: 2, pot: false, square: false}); 355 | }); 356 | 357 | test("sets all elements along width with the smallest height", () => { 358 | /** 359 | * Visualize the placement result 360 | * _______________________ 361 | * | ███ ███ ███ | 362 | * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 363 | */ 364 | 365 | let position1 = bin.add(300, 50, {}); 366 | let position2 = bin.add(50, 300, {}); 367 | let position3 = bin.add(300, 50, {}); 368 | expect(position1.x).toBe(0); 369 | expect(position1.y).toBe(0); 370 | expect(position2.x).toBe(300); 371 | expect(position2.y).toBe(0); 372 | expect(position3.x).toBe(600); 373 | expect(position3.y).toBe(0); 374 | expect(bin.width).toBe(900); 375 | expect(bin.height).toBe(50); 376 | }); 377 | 378 | test("adds rects correctly with rotation", () => { 379 | /** 380 | * Visualize the placement result (1 vertical at the end) 381 | * _______________________ 382 | * | ███ ███ ███ █ | 383 | * | ███ ███ ███ █ | 384 | * | ███ ███ ███ █ | 385 | * | ██████ | 386 | * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 387 | */ 388 | 389 | const rects = [ 390 | [300, 100], 391 | [100, 300], 392 | [300, 100], 393 | [300, 100], 394 | [100, 300], 395 | [300, 100], 396 | [300, 100], 397 | [100, 300], 398 | [300, 100], 399 | [300, 100], 400 | [300, 100], 401 | [100, 600], 402 | ] 403 | rects.forEach(rect => bin.add(rect[0], rect[1])); 404 | expect([bin.rects[0].x, bin.rects[0].y]).toEqual([0, 0]); 405 | expect([bin.rects[1].x, bin.rects[1].y]).toEqual([300, 0]); 406 | expect([bin.rects[2].x, bin.rects[2].y]).toEqual([600, 0]); 407 | expect([bin.rects[3].x, bin.rects[3].y]).toEqual([0, 100]); 408 | expect([bin.rects[4].x, bin.rects[4].y]).toEqual([300, 100]); 409 | expect([bin.rects[5].x, bin.rects[5].y]).toEqual([600, 100]); 410 | expect([bin.rects[6].x, bin.rects[6].y]).toEqual([0, 200]); 411 | expect([bin.rects[7].x, bin.rects[7].y]).toEqual([300, 200]); 412 | expect([bin.rects[8].x, bin.rects[8].y]).toEqual([600, 200]); 413 | expect([bin.rects[9].x, bin.rects[9].y]).toEqual([900, 0]); 414 | expect([bin.rects[10].x, bin.rects[10].y]).toEqual([0, 300]); 415 | expect(bin.width).toBe(1000); 416 | expect(bin.height).toBe(400); 417 | }); 418 | 419 | }); -------------------------------------------------------------------------------- /test/maxrects-packer.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let MaxRectsPacker = require("../dist/maxrects-packer").MaxRectsPacker; 4 | let PACKING_LOGIC = require("../dist/maxrects-packer").PACKING_LOGIC; 5 | let Rectangle = require("../dist/maxrects-packer").Rectangle; 6 | 7 | const opt = { 8 | smart: true, 9 | pot: false, 10 | square: false, 11 | allowRotation: false, 12 | tag: false, 13 | exclusiveTag: true 14 | }; 15 | 16 | let packer; 17 | beforeEach(() => { 18 | packer = new MaxRectsPacker(1024, 1024, 0, opt); 19 | }); 20 | 21 | describe("#add", () => { 22 | test("adds first element correctly", () => { 23 | packer.add(1000, 1000, {num: 1}); 24 | expect(packer.bins[0].rects[0].data.num).toBe(1); 25 | }); 26 | 27 | test("creates additional bin if element doesn't fit in existing bin", () => { 28 | packer.add(1000, 1000, {num: 1}); 29 | packer.add(1000, 1000, {num: 2}); 30 | expect(packer.bins.length).toBe(2); 31 | expect(packer.bins[1].rects[0].data.num).toBe(2); 32 | }); 33 | 34 | test("adds to existing bins if possible", () => { 35 | packer.add(1000, 1000, {num: 1}); 36 | packer.add(1000, 1000, {num: 2}); 37 | packer.add(10, 10, {num: 3}); 38 | packer.add(10, 10, {num: 4}); 39 | expect(packer.bins.length).toBe(2); 40 | }); 41 | 42 | test("adds to new bins after next() is called", () => { 43 | packer.add(1000, 1000, {num: 1}); 44 | packer.add(1000, 1000, {num: 2}); 45 | packer.next(); 46 | packer.add(10, 10, {num: 3}); 47 | packer.add(10, 10, {num: 4}); 48 | expect(packer.bins.length).toBe(3); 49 | expect(packer.bins[packer.bins.length - 1].rects.length).toBe(2); 50 | }); 51 | 52 | test("adds to bins with tag matching on", () => { 53 | packer.options.tag = true; 54 | packer.add(1000, 1000, {num: 1}); 55 | packer.add(10, 10, {num: 2}); 56 | packer.add(1000, 1000, {num: 3, tag: "one"}); 57 | packer.add(1000, 1000, {num: 4, tag: "one"}); 58 | packer.add(10, 10, {num: 5, tag: "one"}); 59 | packer.add(10, 10, {num: 6, tag: "one"}); 60 | packer.add(10, 10, {num: 7, tag: "two"}); 61 | packer.next(); 62 | packer.add(10, 10, {num: 8, tag: "two"}); 63 | expect(packer.bins.length).toBe(5); 64 | expect(packer.bins[0].rects.length).toBe(2); 65 | expect(packer.bins[0].tag).toBeUndefined(); 66 | expect(packer.bins[1].rects.length).toBe(3); 67 | expect(packer.bins[1].tag).toBe("one"); 68 | expect(packer.bins[2].rects.length).toBe(1); 69 | expect(packer.bins[2].tag).toBe("one"); 70 | expect(packer.bins[packer.bins.length - 1].rects.length).toBe(1); 71 | expect(packer.bins[packer.bins.length - 1].tag).toBe("two"); 72 | }); 73 | 74 | test("adds to bins with tag matching disable", () => { 75 | packer.options.tag = false; 76 | packer.add(1000, 1000, {num: 1}); 77 | packer.add(10, 10, {num: 2}); 78 | packer.add(1000, 1000, {num: 3, tag: "one"}); 79 | packer.add(10, 10, {num: 4, tag: "two"}); 80 | packer.next(); 81 | packer.add(10, 10, {num: 8, tag: "two"}); 82 | expect(packer.bins.length).toBe(3); 83 | expect(packer.bins[0].tag).toBeUndefined(); 84 | expect(packer.bins[1].tag).toBeUndefined(); 85 | expect(packer.bins[packer.bins.length - 1].rects.length).toBe(1); 86 | expect(packer.bins[packer.bins.length - 1].tag).toBeUndefined(); 87 | }); 88 | 89 | test("adds to bins with non-exclusive tag matching", () => { 90 | packer.options = {...packer.options, ...{tag: true, exclusiveTag: false}}; 91 | let input = [ 92 | {width: 512, height: 512, data: {}}, 93 | {width: 512, height: 512, data: {tag: "one"}}, 94 | {width: 512, height: 512, data: {tag: "two"}}, 95 | {width: 512, height: 512, data: {tag: "two"}}, 96 | {width: 512, height: 512, data: {tag: "two"}}, 97 | // Will break into its own bin 98 | {width: 600, height: 600, data: {tag: "two"}}, 99 | {width: 512, height: 512, data: {tag: "two"}}, 100 | {width: 512, height: 512, data: {tag: "one"}}, 101 | {width: 512, height: 512, data: {}} 102 | ]; 103 | packer.addArray(input); 104 | expect(packer.bins.length).toBe(3); 105 | expect(packer.bins[0].tag).toBeUndefined(); 106 | expect(packer.bins[1].tag).toBeUndefined(); 107 | expect(packer.bins[2].tag).toBeUndefined(); 108 | expect(packer.bins[0].rects.length).toBe(4); 109 | expect(packer.bins[1].rects.length).toBe(4); 110 | expect(packer.bins[2].rects.length).toBe(1); 111 | expect(packer.bins[0].rects[0].data.tag).toBe("one"); 112 | expect(packer.bins[0].rects[1].data.tag).toBe("one"); 113 | expect(packer.bins[0].rects[2].data.tag).toBe("two"); 114 | expect(packer.bins[0].rects[3].data.tag).toBe("two"); 115 | expect(packer.bins[1].rects[0].data.tag).toBe("two"); 116 | expect(packer.bins[1].rects[1].data.tag).toBe("two"); 117 | expect(packer.bins[1].rects[2].data.tag).toBeUndefined(); 118 | expect(packer.bins[1].rects[3].data.tag).toBeUndefined(); 119 | expect(packer.bins[2].rects[0].data.tag).toBe("two"); 120 | }); 121 | 122 | test("allows oversized elements to be added", () => { 123 | packer.add(1000, 1000, {num: 1}); 124 | packer.add(2000, 2000, {num: 2}); 125 | expect(packer.bins.length).toBe(2); 126 | expect(packer.bins[1].rects[0].width).toBe(2000); 127 | expect(packer.bins[1].rects[0].oversized).toBe(true); 128 | }); 129 | 130 | test("checks oversized elements rotation and adds rotated", () => { 131 | const packer = new MaxRectsPacker(512, 1024, 0, {...opt, allowRotation: true}); 132 | packer.add(640, 256, {num: 1}); 133 | expect(packer.bins.length).toBe(1); 134 | expect(packer.bins[0].rects[0].width).toBe(256); 135 | expect(packer.bins[0].rects[0].height).toBe(640); 136 | expect(packer.bins[0].rects[0].oversized).toBe(false); 137 | }); 138 | 139 | test("checks oversized elements and skip rotation when set to false", () => { 140 | const packer = new MaxRectsPacker(512, 1024, 0, {...opt, allowRotation: false}); 141 | packer.add(640, 256, {num: 1}); 142 | expect(packer.bins.length).toBe(1); 143 | expect(packer.bins[0].rects[0].width).toBe(640); 144 | expect(packer.bins[0].rects[0].oversized).toBe(true); 145 | }); 146 | }); 147 | 148 | describe("#sort", () => { 149 | test("does not mutate input array", () => { 150 | let input = [ 151 | {width: 1, height: 1}, 152 | {width: 2, height: 2} 153 | ]; 154 | packer.sort(input); 155 | expect(input[0].width).toBe(1); 156 | }); 157 | 158 | test("works correctly by area", () => { 159 | let input = [ 160 | {width: 1, height: 1}, 161 | {width: 3, height: 1}, 162 | {width: 2, height: 2} 163 | ]; 164 | let output = packer.sort(input, PACKING_LOGIC.MAX_AREA); 165 | expect(output[0].width).toBe(2); 166 | expect(output[1].width).toBe(3); 167 | expect(output[2].width).toBe(1); 168 | }); 169 | 170 | test("works correctly by edge", () => { 171 | let input = [ 172 | {width: 1, height: 1}, 173 | {width: 3, height: 1}, 174 | {width: 2, height: 2} 175 | ]; 176 | let output = packer.sort(input, PACKING_LOGIC.MAX_EDGE); 177 | expect(output[0].width).toBe(3); 178 | expect(output[1].width).toBe(2); 179 | expect(output[2].width).toBe(1); 180 | }); 181 | }); 182 | 183 | describe("#addArray", () => { 184 | test("adds multiple elements to bins", () => { 185 | let input = [ 186 | {width: 1000, height: 1000, data: {num: 1}}, 187 | {width: 1000, height: 1000, data: {num: 2}} 188 | ]; 189 | packer.addArray(input); 190 | expect(packer.bins.length).toBe(2); 191 | }); 192 | 193 | test("adds the big rects first", () => { 194 | let input = [ 195 | {width: 600, height: 20, data: {num: 1}}, 196 | {width: 600, height: 20, data: {num: 2}}, 197 | {width: 1000, height: 1000, data: {num: 3}}, 198 | {width: 1000, height: 1000, data: {num: 4}} 199 | ]; 200 | packer.addArray(input); 201 | expect(packer.bins.length).toBe(2); 202 | }); 203 | 204 | test("adds the big rects & big hash first", () => { 205 | let input = [ 206 | {width: 600, height: 20, data: {num: 1}, hash: "aaa"}, 207 | {width: 600, height: 20, data: {num: 2}, hash: "bbb"}, 208 | {width: 1000, height: 1000, data: {num: 3}, hash: "ccc"}, 209 | {width: 1000, height: 1000, data: {num: 4}, hash: "ddd"} 210 | ]; 211 | packer.addArray(input); 212 | expect(packer.bins.length).toBe(2); 213 | expect(packer.bins[0].rects[0].hash).toBe("ddd"); 214 | expect(packer.bins[0].rects[1].hash).toBe("bbb"); 215 | }); 216 | 217 | test("add empty array", () => { 218 | packer.addArray([]); // test null array error 219 | expect(packer.bins.length).toBe(0); 220 | }); 221 | 222 | test("add one element array", () => { 223 | let input = [ 224 | {width: 600, height: 20, data: {num: 1}, hash: "aaa"} 225 | ]; 226 | packer.addArray(input); // test null array error 227 | expect(packer.bins.length).toBe(1); 228 | }); 229 | }); 230 | 231 | describe("#save & load", () => { 232 | test("Load old bins and continue packing", () => { 233 | packer.options.tag = true; 234 | let input = [ 235 | {width: 512, height: 512, data: {num: 1}}, 236 | {width: 512, height: 512, data: {num: 2}, tag: "one"}, 237 | {width: 512, height: 512, data: {num: 3}, tag: "one"}, 238 | {width: 512, height: 512, data: {num: 4}}, 239 | ]; 240 | packer.addArray(input); 241 | expect(packer.bins.length).toBe(2); 242 | expect(packer.bins[0].rects.length).toBe(2); 243 | expect(packer.bins[1].rects.length).toBe(2); 244 | let bins = packer.save(); 245 | expect(bins[0].rects.length).toBe(0); 246 | expect(bins[1].tag).toBe("one"); 247 | packer.load(bins); 248 | packer.addArray(input); 249 | expect(packer.bins.length).toBe(2); 250 | expect(packer.bins[0].rects.length).toBe(2); 251 | expect(packer.bins[1].rects.length).toBe(2); 252 | expect(packer.bins[1].tag).toBe("one"); 253 | }); 254 | }); 255 | 256 | describe("misc functionalities", () => { 257 | test("passes padding through", () => { 258 | packer = new MaxRectsPacker(1024, 1024, 4, opt); 259 | packer.add(500, 500, {num: 1}); 260 | packer.add(500, 500, {num: 1}); 261 | packer.add(500, 500, {num: 1}); 262 | expect(packer.bins[0].width).toBe(1004); 263 | }); 264 | 265 | test("get bins dirty status", () => { 266 | packer = new MaxRectsPacker(1024, 1024, 4, opt); 267 | packer.add(508, 508, {num: 1}); 268 | let tester1 = packer.add(new Rectangle(508, 508)); 269 | let tester2 = packer.add(508, 508, {num: 3}); 270 | expect(packer.dirty).toBe(true); 271 | for (let bin of packer.bins) bin.setDirty(false); 272 | expect(packer.dirty).toBe(false); 273 | tester1.height = 512; 274 | expect(packer.dirty).toBe(true); 275 | for (let bin of packer.bins) bin.setDirty(false); 276 | tester2.height = 512; 277 | expect(packer.dirty).toBe(true); 278 | }); 279 | 280 | test("quick repack & deep repack", () => { 281 | packer = new MaxRectsPacker(1024, 1024, 0, {...opt, ...{ tag: true }}); 282 | let rect = packer.add(1024, 512, {hash: "6"}); 283 | packer.add(512, 512, {hash: "5"}); 284 | packer.add(512, 512, {hash: "4"}); 285 | packer.add(512, 512, {hash: "3"}); 286 | packer.add({width: 512, height: 512, data: {hash: "2", tag: "one"}}); 287 | packer.add({width: 512, height: 512, data: {hash: "1"}, tag: "one"}); 288 | expect(packer.bins.length).toBe(3); 289 | for (let bin of packer.bins) bin.setDirty(false); // clean dirty status 290 | rect.width = 512; // shrink width 291 | packer.repack(); // quick repack 292 | expect(packer.bins.length).toBe(3); 293 | packer.repack(false); // deep repack 294 | expect(packer.bins.length).toBe(2); 295 | rect.height = 1024; // enlarge height 296 | rect.tag = "one"; 297 | packer.repack(); // quick repack 298 | expect(packer.bins.length).toBe(3); 299 | expect(packer.bins[2].tag).toBe("one"); 300 | packer.repack(false); // deep repack 301 | expect(packer.bins.length).toBe(2); 302 | }); 303 | 304 | test("Packer allow rotation", () => { 305 | packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: true }}); 306 | packer.add(398, 98); 307 | packer.add(398, 98); 308 | packer.add(398, 98); 309 | let x = packer.add(398, 98); 310 | expect(x.rot).toBe(true); 311 | }); 312 | 313 | test("Per rectangle allow rotation", () => { 314 | packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: true }}); 315 | packer.add(448, 98); 316 | packer.add(448, 98); 317 | packer.add(448, 98); 318 | packer.add(448, 98); 319 | // false overriding 320 | let x = packer.add(398, 48, { allowRotation: false }); 321 | expect(packer.bins.length).toBe(2); 322 | expect(x.rot).toBe(false); 323 | 324 | packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: false}}); 325 | packer.add(448, 98); 326 | packer.add(448, 98); 327 | packer.add(448, 98); 328 | packer.add(448, 98); 329 | // true overriding 330 | x = packer.add(398, 48, { allowRotation: true }); 331 | expect(packer.bins.length).toBe(1); 332 | expect(x.rot).toBe(true); 333 | }); 334 | }); 335 | -------------------------------------------------------------------------------- /test/oversized-element-bin.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let OversizedElementBin = require("../dist/maxrects-packer").OversizedElementBin; 4 | let Rectangle = require("../dist/maxrects-packer").Rectangle; 5 | 6 | const oversizedRect = new Rectangle(2000, 2000); 7 | oversizedRect.data = {foo: "bar"}; 8 | 9 | describe("OversizedElementBin", () => { 10 | test("stores data correctly", () => { 11 | let bin = new OversizedElementBin(2000, 2000, {foo: "bar"}); 12 | expect(bin.width).toBe(2000); 13 | expect(bin.height).toBe(2000); 14 | expect(bin.rects[0].x).toBe(0); 15 | expect(bin.rects[0].y).toBe(0); 16 | expect(bin.rects[0].width).toBe(2000); 17 | expect(bin.rects[0].height).toBe(2000); 18 | expect(bin.rects[0].data.foo).toBe("bar"); 19 | expect(bin.rects[0].oversized).toBeTruthy(); 20 | }); 21 | 22 | test("stores data correctly via generic type", () => { 23 | let bin = new OversizedElementBin(oversizedRect); 24 | expect(bin.width).toBe(2000); 25 | expect(bin.height).toBe(2000); 26 | expect(bin.rects[0].x).toBe(0); 27 | expect(bin.rects[0].y).toBe(0); 28 | expect(bin.rects[0].width).toBe(2000); 29 | expect(bin.rects[0].height).toBe(2000); 30 | expect(bin.rects[0].data.foo).toBe("bar"); 31 | expect(bin.rects[0].oversized).toBeTruthy(); 32 | }); 33 | 34 | test("#add returns undefined", () => { 35 | let bin = new OversizedElementBin(2000, 2000, {foo: "bar"}); 36 | expect(bin.add(1, 1, {})).toBeUndefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/rectangle.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Rectangle = require("../dist/maxrects-packer").Rectangle; 4 | 5 | const bigRect = new Rectangle(512, 512, 0, 0); 6 | const containedRect = new Rectangle(256, 256, 16, 128); 7 | const collideRect = new Rectangle(256, 256, 384, 128); 8 | 9 | describe("Rectangle", () => { 10 | test("Default value", () => { 11 | const rect = new Rectangle(); 12 | expect(rect.width).toBe(0); 13 | expect(rect.height).toBe(0); 14 | expect(rect.x).toBe(0); 15 | expect(rect.y).toBe(0); 16 | expect(rect.dirty).toBe(false); 17 | }); 18 | 19 | test("Specified value", () => { 20 | const rect = new Rectangle(512, 512, 16); 21 | expect(rect.width).toBe(512); 22 | expect(rect.height).toBe(512); 23 | expect(rect.x).toBe(16); 24 | expect(rect.y).toBe(0); 25 | expect(rect.dirty).toBe(false); 26 | }); 27 | 28 | test("Dynamiclly changing value", () => { 29 | const rect = new Rectangle(512, 512, 16); 30 | rect.width = 256; 31 | rect.y = 32; 32 | expect(rect.width).toBe(256); 33 | expect(rect.y).toBe(32); 34 | expect(rect.dirty).toBe(true); 35 | }); 36 | 37 | test("Report dirty status correctly", () => { 38 | const rect = new Rectangle(512, 512, 16); 39 | rect.width = 256; 40 | expect(rect.dirty).toBe(true); 41 | rect.setDirty(false); 42 | expect(rect.dirty).toBe(false); 43 | rect.y = 32; 44 | expect(rect.dirty).toBe(true); 45 | rect.setDirty(false); 46 | rect.data = {foo: "bar"}; 47 | expect(rect.dirty).toBe(true); 48 | }); 49 | 50 | test("Rot flag functionality", () => { 51 | const rect = new Rectangle(512, 256); 52 | expect(rect.rot).toBe(false); 53 | rect.rot = true; 54 | expect(rect.rot).toBe(true); 55 | expect(rect.width).toBe(256); 56 | expect(rect.height).toBe(512); 57 | expect(rect.dirty).toBe(true); 58 | rect.setDirty(false); 59 | rect.rot = true; 60 | expect(rect.width).toBe(256); 61 | expect(rect.height).toBe(512); 62 | expect(rect.dirty).toBe(false); 63 | rect.rot = false; 64 | expect(rect.rot).toBe(false); 65 | expect(rect.width).toBe(512); 66 | expect(rect.height).toBe(256); 67 | expect(rect.dirty).toBe(true); 68 | }); 69 | 70 | test("allowRotation setting", () => { 71 | const rect = new Rectangle(512, 256, 0, 0, false, true); 72 | expect(rect.allowRotation).toBe(true); 73 | rect.allowRotation = false; 74 | expect(rect.allowRotation).toBe(false); 75 | }); 76 | 77 | test("data.allowRotation sync", () => { 78 | const rect = new Rectangle(512, 256); 79 | expect(rect.allowRotation).toBeUndefined(); 80 | rect.data = { allowRotation: false }; 81 | expect(rect.allowRotation).toBe(false); 82 | rect.data = { allowRotation: true }; 83 | expect(rect.allowRotation).toBe(true); 84 | }); 85 | 86 | test("method: area()", () => { 87 | const rect = new Rectangle(16, 16); 88 | expect(rect.area()).toBe(256); 89 | }); 90 | 91 | test("method: collide()", () => { 92 | expect(bigRect.collide(collideRect)).toBe(true); 93 | expect(bigRect.collide(containedRect)).toBe(true); 94 | expect(containedRect.collide(collideRect)).toBe(false); 95 | expect(Rectangle.Collide(bigRect, collideRect)).toBe(true); 96 | }); 97 | 98 | test("method: contain()", () => { 99 | expect(bigRect.contain(collideRect)).toBe(false); 100 | expect(bigRect.contain(containedRect)).toBe(true); 101 | expect(containedRect.contain(collideRect)).toBe(false); 102 | expect(Rectangle.Contain(bigRect, containedRect)).toBe(true); 103 | expect(Rectangle.Contain(bigRect, collideRect)).toBe(false); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "esModuleInterop": true, 7 | "lib": ["esnext"], /* Specify library files to be included in the compilation: */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationDir": "./lib/types", 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./lib", /* Redirect output structure to the directory. */ 16 | "typeRoots": [ 17 | "lib" 18 | ], 19 | "baseUrl": ".", 20 | "paths": { 21 | "*": [ 22 | "node_modules/*", 23 | "lib/*" 24 | ] 25 | }, 26 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 27 | // "removeComments": true, /* Do not emit comments to output. */ 28 | // "noEmit": true, /* Do not emit outputs. */ 29 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 30 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 31 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 32 | 33 | /* Strict Type-Checking Options */ 34 | "strict": true, /* Enable all strict type-checking options. */ 35 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 36 | // "strictNullChecks": true, /* Enable strict null checks. */ 37 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 38 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 39 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 40 | 41 | /* Additional Checks */ 42 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 43 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 44 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 45 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 46 | 47 | /* Module Resolution Options */ 48 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 49 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 50 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | "types": ["node"] /* Type declaration files to be included in compilation. */ 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | }, 67 | "typedocOptions": { 68 | "mode": "modules", 69 | "out": "docs", 70 | "excludeExternals": true, 71 | "excludeNotExported": true, 72 | "excludePrivate": true 73 | }, 74 | "include": [ 75 | "./src/**/*", 76 | "./test/**/*", 77 | "./*.js", 78 | ] 79 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "ter-indent": 4, 5 | "semicolon": true, 6 | "quotemark": false 7 | } 8 | } --------------------------------------------------------------------------------