├── image-resource
└── max-rect-time.png
├── test
├── data.js
├── rects.json
├── index.js
├── show-result.html
├── charts.html
├── index.ts
└── rects1.json
├── .editorconfig
├── tslint.json
├── src
├── index.ts
├── rect.ts
├── search.ts
├── genetic.ts
└── max-rect-bin-pack.ts
├── tsconfig.json
├── tsconfig.esm.json
├── LICENSE
├── .gitignore
├── package.json
└── README.md
/image-resource/max-rect-time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ant-tinyjs/bin-packing-core/HEAD/image-resource/max-rect-time.png
--------------------------------------------------------------------------------
/test/data.js:
--------------------------------------------------------------------------------
1 | var data = [{"x":0,"y":0,"width":485,"height":869,"isRotated":false},{"x":0,"y":869,"width":96,"height":104,"isRotated":false}]
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/test/rects.json:
--------------------------------------------------------------------------------
1 | [
2 | { "width": 211, "height": 137 },
3 | { "width": 259, "height": 165 },
4 | { "width": 266, "height": 122 },
5 | { "width": 203, "height": 115 },
6 | { "width": 265, "height": 162 },
7 | { "width": 261, "height": 106 }
8 | ]
9 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {},
5 | "rules": {
6 | "arrow-parens": false,
7 | "quotemark": [true, "single", "avoid-escape", "avoid-template"],
8 | "no-var-requires": false,
9 | "unified-signatures": false
10 | },
11 | "rulesDirectory": []
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import genetic, { Genetic, IGeneticOptions } from './genetic';
2 | import MaxRectBinPack, { FindPosition } from './max-rect-bin-pack';
3 | import Rect from './rect';
4 | import { Search } from './search';
5 |
6 | export {
7 | FindPosition,
8 | Genetic, // 遗传算法类
9 | MaxRectBinPack,
10 | Rect,
11 | genetic, // 包装方法
12 | Search,
13 | };
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "allowJs": false,
7 | "alwaysStrict": true,
8 | "importHelpers": false,
9 | "lib": [
10 | "es5",
11 | "es6"
12 | ],
13 | "module": "umd",
14 | "moduleResolution": "node",
15 | "noEmitOnError": true,
16 | "noImplicitThis": true,
17 | "noImplicitAny": true,
18 | "outDir": "./lib",
19 | "sourceMap": true,
20 | "target": "es5",
21 | "typeRoots": [
22 | "types",
23 | "node_modules/@types"
24 | ]
25 | },
26 | "include": [
27 | "src"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "esModuleInterop": true,
6 | "allowJs": false,
7 | "alwaysStrict": true,
8 | "importHelpers": false,
9 | "lib": [
10 | "es5",
11 | "es6"
12 | ],
13 | "module": "es6",
14 | "moduleResolution": "node",
15 | "noEmitOnError": true,
16 | "noImplicitThis": true,
17 | "noImplicitAny": true,
18 | "outDir": "./esm",
19 | "sourceMap": true,
20 | "target": "es5",
21 | "typeRoots": [
22 | "types",
23 | "node_modules/@types"
24 | ]
25 | },
26 | "include": [
27 | "src"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var FindPosition = require('./../lib/index').FindPosition;
2 | var Genetic = require('./../lib/index').Genetic;
3 | var genetic = require('./../lib/index').genetic;
4 | var MaxRectBinPack = require('./../lib/index').MaxRectBinPack;
5 | var Rect = require('./../lib/index').Rect;
6 |
7 |
8 |
9 | const findPosition = 0; // 参照第一条
10 |
11 | const bestSize = genetic(rects, {
12 | // 遗传算法确定最优策略
13 | findPosition: findPosition,
14 | lifeTimes: 50, // 代数
15 | liveRate: 0.5, // 存活率
16 | size: 50, // 每一代孩子个数
17 | });
18 |
19 | const width = bestSize.x;
20 | const height = bestSize.y;
21 | const packer = new MaxRectBinPack(width, height, true);
22 | const result = packer.insertRects(rects, findPosition);
23 | console.log(result);
24 | console.log(result.length === 10);
25 |
--------------------------------------------------------------------------------
/test/show-result.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/rect.ts:
--------------------------------------------------------------------------------
1 | class Rect {
2 | /**
3 | * 起点 x 坐标
4 | */
5 | public x: number = 0;
6 | /**
7 | * 起点 y 坐标
8 | */
9 | public y: number = 0;
10 | /**
11 | * 宽度
12 | */
13 | public width: number = 0;
14 | /**
15 | * 高度
16 | */
17 | public height: number = 0;
18 | /**
19 | * 当前是否被旋转了
20 | */
21 | public isRotated: boolean = false;
22 | /**
23 | * 自定义信息
24 | */
25 | public info: any;
26 | /**
27 | * 克隆
28 | */
29 | public clone(): Rect {
30 | const cloned = new Rect();
31 | cloned.x = this.x;
32 | cloned.y = this.y;
33 | cloned.height = this.height;
34 | cloned.width = this.width;
35 | cloned.info = this.info;
36 | return cloned;
37 | }
38 | /**
39 | * 矩形是否在另一个矩形内部
40 | * @param otherRect {Rect}
41 | */
42 | public isIn(otherRect: Rect): boolean {
43 | return (
44 | this.x >= otherRect.x &&
45 | this.y >= otherRect.y &&
46 | this.x + this.width <= otherRect.x + otherRect.width &&
47 | this.y + this.height <= otherRect.y + otherRect.height
48 | );
49 | }
50 | }
51 |
52 | export default Rect;
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 zoolsher
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 |
--------------------------------------------------------------------------------
/.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 (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | .vscode/
64 |
65 | esm
66 |
67 | lib
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bin-packing-core",
3 | "version": "0.2.0-beta02",
4 | "description": "image packer based on genetic & max-rect algorithm",
5 | "main": "lib/index.js",
6 | "module": "esm/index.js",
7 | "files": [
8 | "lib",
9 | "esm"
10 | ],
11 | "scripts": {
12 | "dev": "tsc -p ./tsconfig.json --watch ",
13 | "test": "mocha -r ts-node/register test/**/**.ts",
14 | "build": "npm run build:es5 && npm run build:es6",
15 | "build:es5": "tsc -p ./tsconfig.json -d",
16 | "build:es6": "tsc -p ./tsconfig.esm.json -d",
17 | "lint": "tslint --project ./tsconfig.json",
18 | "lint:fix": "tslint --fix --project ./tsconfig.json",
19 | "prepublish": "tnpm run build"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+git@github.com:ant-tinyjs/bin-packing-core.git"
24 | },
25 | "author": "",
26 | "license": "ISC",
27 | "bugs": {
28 | "url": "https://github.com/ant-tinyjs/bin-packing-core/issues"
29 | },
30 | "homepage": "https://github.com/ant-tinyjs/bin-packing-core#readme",
31 | "dependencies": {},
32 | "devDependencies": {
33 | "@types/chai": "^4.1.3",
34 | "@types/mocha": "^5.2.2",
35 | "@types/node": "^10.3.2",
36 | "chai": "^4.1.2",
37 | "mocha": "^5.2.0",
38 | "tslint": "^5.8.0",
39 | "ts-node": "^6.1.1",
40 | "typescript": "^2.9.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/charts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | //tslint:disable
2 | import fs from 'fs';
3 | import path from 'path';
4 | import genetic from './../src/genetic';
5 | import MaxRectBinPack, { FindPosition } from './../src/max-rect-bin-pack';
6 | import Rect from './../src/rect';
7 | import { Search } from './../src/search';
8 |
9 | const rects: Rect[] = [];
10 |
11 | // const r = require('./rects.json');
12 |
13 | // r.forEach(($: any) => {
14 | // var rect = new Rect();
15 | // rect.width = $.width;
16 | // rect.height = $.height;
17 | // rect.info = {};
18 | // rect.info.width = $.width;
19 | // rect.info.height = $.height;
20 | // rects.push(rect);
21 | // });
22 |
23 | var rect1 = new Rect();
24 | rect1.width = 485;
25 | rect1.height = 869;
26 | rects.push(rect1);
27 |
28 | var rect2 = new Rect();
29 | rect2.width = 96;
30 | rect2.height = 104;
31 | rects.push(rect2);
32 |
33 | // function getRects() {
34 | // return rects.map($ => $.clone());
35 | // }
36 |
37 | // let maxWidth = rects.reduce(($, $$) => $ + $$.width, 0);
38 | // let maxHeight = rects.reduce(($, $$) => $ + $$.height, 0);
39 | // debugger;
40 | // const step = 1;
41 | // const result = [];
42 | // for (let currentWidth = 0; currentWidth <= maxWidth; currentWidth += step) {
43 | // for (
44 | // let currentHeight = 0;
45 | // currentHeight <= maxHeight;
46 | // currentHeight += step
47 | // ) {
48 | // const packer = new MaxRectBinPack(currentWidth, currentHeight, false);
49 | // const inserts = packer.insertRects(getRects(), 0);
50 | // if (inserts.length === 2) {
51 | // console.log(currentWidth, currentHeight);
52 | // result.push({
53 | // width: currentWidth,
54 | // height: currentHeight,
55 | // op: packer.occupancy(),
56 | // });
57 | // }
58 | // }
59 | // }
60 | // fs.writeFileSync(
61 | // path.join(__dirname, 'data.js'),
62 | // `var data = ${JSON.stringify(result)}`,
63 | // {
64 | // flag: 'w+',
65 | // },
66 | // );
67 | // debugger;
68 | const serach = new Search(rects, false, 10, 0, Infinity);
69 | const bestNode = serach.search();
70 |
71 | console.log(bestNode);
72 | const packer = new MaxRectBinPack(bestNode.x, bestNode.y, false);
73 |
74 | let rectslength = rects.length;
75 | console.log(rectslength);
76 | const result = packer.insertRects(rects, FindPosition.AreaFit);
77 | console.log(JSON.stringify(result));
78 | console.log(packer.occupancy());
79 | console.log(result.length);
80 | console.log(bestNode.x * bestNode.y);
81 | console.log(result.length === rectslength);
82 | fs.writeFileSync(
83 | path.join(__dirname, 'data.js'),
84 | `var data = ${JSON.stringify(result)}`,
85 | {
86 | flag: 'w+',
87 | },
88 | );
89 |
--------------------------------------------------------------------------------
/src/search.ts:
--------------------------------------------------------------------------------
1 | import MaxRectBinPack, { FindPosition } from './max-rect-bin-pack';
2 | import Rect from './rect';
3 |
4 | interface IPoint {
5 | x: number;
6 | y: number;
7 | }
8 |
9 | export class Search {
10 | private rects: Rect[] = [];
11 | private findPosition: FindPosition;
12 | private allowRotate: boolean;
13 | private step: number;
14 | private rate: number;
15 |
16 | private totalSquares: number = 0;
17 | private maxHeight: number = -1;
18 | private maxWidth: number = -1;
19 |
20 | get minHeight() {
21 | return this.totalSquares / this.maxWidth;
22 | }
23 | get minWidth() {
24 | return this.totalSquares / this.maxHeight;
25 | }
26 | /**
27 | * 初始化
28 | * @param rects 要插入的矩形数组
29 | * @param allowRotate 是否旋转
30 | * @param step 搜索步长 建议10
31 | * @param findPosition FindPostion 策略
32 | * @param rate 大于一的比率 等于1不可以的
33 | */
34 | constructor(rects: Rect[], allowRotate: boolean, step: number, findPosition: FindPosition, rate: number) {
35 | this.rects = rects;
36 | this.allowRotate = allowRotate === true;
37 | this.step = step ? step : 1;
38 | this.findPosition = findPosition;
39 | if (rate <= 1) {
40 | throw new Error('rate should be grater than 1, but get ' + rate);
41 | }
42 | this.rate = rate;
43 | this.totalSquares = this.rects.reduce((i, v) => {
44 | return i + v.height * v.width;
45 | }, 0);
46 | this.maxWidth = this.rects.reduce((i, v) => {
47 | return i + v.width;
48 | }, this.step + 1); // 防止刚好踩到临界点情况
49 | this.maxHeight = this.rects.reduce((i, v) => {
50 | return i + v.height;
51 | }, this.step + 1); // 防止刚好踩到临界点情况
52 | }
53 | public search(): IPoint {
54 | const bestResult = {
55 | height: 0,
56 | op: 0,
57 | width: 0,
58 | };
59 | for (let searchWidth = this.minWidth; searchWidth <= this.maxWidth; searchWidth += this.step) {
60 | const [height, op] = this.bestHeight(searchWidth);
61 | if (op > bestResult.op) {
62 | bestResult.width = searchWidth;
63 | bestResult.height = height;
64 | bestResult.op = op;
65 | }
66 | }
67 | return { x: bestResult.width, y: bestResult.height };
68 | }
69 | public bestHeight(width: number): [number, number] {
70 | let left = Math.max(this.minHeight, width / this.rate);
71 | let right = Math.min(this.maxHeight, width * this.rate);
72 | let bestResult = 0;
73 | let mid = 0;
74 | let bestHeight = 0;
75 | while (right - left >= this.step) {
76 | mid = Math.ceil((right + left) / 2);
77 | const [result, op] = this.getInsertResult(width, mid);
78 | const isSuccess = result.length === this.rects.length;
79 | if (isSuccess) {
80 | if (op > bestResult) {
81 | bestResult = op;
82 | bestHeight = mid;
83 | }
84 | right = mid;
85 | } else {
86 | left = mid;
87 | }
88 | }
89 | return [bestHeight, bestResult];
90 | }
91 | private getInsertResult(width: number, height: number): [Rect[], number] {
92 | const binpacker = new MaxRectBinPack(width, height, this.allowRotate);
93 | const result = binpacker.insertRects(this.getRects(), this.findPosition);
94 | return [result, binpacker.occupancy()];
95 | }
96 | private getRects() {
97 | return this.rects.map($ => {
98 | return $.clone();
99 | });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于人工智能的雪碧图拼接算法
2 |
3 | **keywords:** `bin-pack` `image-pack` `max-rect` `genetic` `typescript` `javascript` `node.js` `image-resource` `ai`
4 |
5 | ## 应用场景
6 |
7 | 前端资源中经常需要将所有的小图合成大图,如何将合成大图的图片压缩到最小,就是本算法要解决的问题。
8 |
9 | 使用本算法可以有效减小图片资源的`解析时长`和`内存占用`。
10 |
11 | 基于目前亿级PV项目验证,可以减小10%~30%的内存占用。
12 |
13 | ## 算法
14 |
15 | **算法分为两层**
16 |
17 | 1. 底层是基于 `max-rect-bin-pack` 的 `Rectangle Bin Pack` 算法
18 | 2. 上层是基于 遗传算法 的最优化的搜索算法。 // 更新了二分法
19 |
20 | **FYI:** 底层算法分为`在线`和`离线`两种模式: 离线算法有一定的优化方案,所以离线算法效果更好。上层的遗传算法强制调用了离线算法。
21 |
22 | **算法的复杂度**
23 |
24 | 1. 底层 `max-rect-bin-pack` 算法
25 |
26 | 
27 |
28 | 在线算法虽然效率更高,但是在线算法结果没有离线算法结果好。
29 |
30 | 2. 上层 遗传算法
31 |
32 | 遗传算法的时间空间复杂参考[paper](https://pdfs.semanticscholar.org/18e1/03c600134d55a4ef084f11bee045b8505cf7.pdf?_ga=2.45871975.213758763.1529380129-1889820781.1529380129)。
33 |
34 | 大致上可以认为和种群中孩子个数和生态个数成正相关。
35 |
36 | ## API & demo
37 |
38 | 1. FindPosition
39 |
40 | 寻找矩形位置的五种策略。
41 |
42 | ```typescript
43 | enum FindPosition {
44 | ShortSideFit = 0,
45 | BottomLeft,
46 | ContactPoint,
47 | LongSideFit,
48 | AreaFit,
49 | }
50 | ```
51 |
52 | 2. Rect
53 |
54 | ```javascript
55 | const rect = new Rect();
56 | rect.width = 90; // 宽度
57 | rect.height = 90; // 高度
58 | rect.x;// x 坐标
59 | rect.y;// y 坐标
60 | rect.isRotated; // 是否被旋转了
61 | rect.info; // 开发者自行写入一些属性,可以在返回值中拿到(基于浅拷贝)
62 | ```
63 |
64 | 3. `max-rect-bin-pack` 调用
65 |
66 | ```javascript
67 | import { MaxRectBinPack, Rect } from 'name';
68 | const width = 200;
69 | const height = 200;
70 | const allowRotate = true;
71 | const packer = MaxRectBinPack(width, height, allowRotate);
72 |
73 | const findPosition = 0; // 参照第一条
74 |
75 | // 在线算法 尽量不要使用
76 | for(const rect of rects){
77 | const result = packer.insert(rect.width, rect.height, findPosition);
78 | if(result.width!==rect.width||result.height!==rect.height){ // 插入失败返回一个宽高为0的Rect
79 | throw new Error('insert failed');
80 | }else{
81 | console.log(result.x, result.y, result.widht, result.height); // 插入成功
82 | }
83 | }
84 |
85 | //离线算法 推荐使用
86 | const rects: Rect[] = [];
87 | const rectsCopy = rects.map($=>$.clone());// js引用传递,这里直接操纵了传入的数组,所以传入一份clone的。
88 | const result = packer.insertRects(rectsCopy, findPosition);
89 | if(result.length!==rects.length){ // 插入失败,因为容器大小不足
90 | throw new Error('insert failed');
91 | }else{ //插入成功
92 | result.forEach(rect => {
93 | console.log(rect.x, rect.y, rect.width, rect.height);
94 | });
95 | }
96 | ```
97 |
98 | 4. `genetic` 遗传算法 调用
99 |
100 | ```typescript
101 | import { MaxRectBinPack, Rect, genetic } from '';
102 | const rects: Rect[] = [];
103 |
104 | const findPosition = 0; // 参照第一条
105 |
106 | const bestSize = genetic(rects, { // 遗传算法确定最优策略
107 | findPosition: findPosition,
108 | lifeTimes: 50, // 代数
109 | liveRate: 0.5, // 存活率
110 | size: 50, // 每一代孩子个数
111 | allowRotate: false,// 不支持旋转
112 | });
113 |
114 | const width = bestSize.x;
115 | const height = bestSize.y;
116 | const packer = new MaxRectBinPack(width, height, false);//不支持旋转
117 | const result = packer.insertRects(rects, findPosition);
118 | console.log(result.length === /* rects.length */);
119 | ```
120 |
121 | 5. `搜索算法`
122 |
123 | ```typescript
124 | /**
125 | * 初始化
126 | * @param rects 要插入的矩形数组
127 | * @param allowRotate 是否旋转
128 | * @param step 搜索步长 建议10
129 | * @param findPosition FindPostion 策略
130 | * @param rate 大于一的比率 等于1不可以的
131 | */
132 | const serach = new Search(rects, false, 10, 0, 1.1);
133 | const bestNode = serach.search();
134 |
135 | console.log(bestNode);
136 | const packer = new MaxRectBinPack(bestNode.x, bestNode.y, false);
137 | const result = packer.insertRects(rects, FindPosition.AreaFit);
138 | ```
139 |
140 | ## TODO
141 |
142 | - [ ] 基因编码策略升级
143 | - [ ] 返回结果优化
144 | - [X] 提升算法稳定性,回归点有多个的时候会出现回归到次点的情况
145 |
--------------------------------------------------------------------------------
/src/genetic.ts:
--------------------------------------------------------------------------------
1 | import MaxRectBinPack, { FindPosition } from './max-rect-bin-pack';
2 | import Rect from './rect';
3 |
4 | export interface IGeneticOptions {
5 | lifeTimes: number;
6 | size: number;
7 | findPosition: FindPosition;
8 | liveRate: number;
9 | allowRotate: boolean;
10 | }
11 |
12 | interface IPoint {
13 | x: number;
14 | y: number;
15 | }
16 |
17 | interface ICalResult {
18 | dot: IPoint;
19 | fitAll: boolean;
20 | occupancy: number;
21 | }
22 |
23 | export class Genetic {
24 | private rects: Rect[] = [];
25 | private lifeTimes: number;
26 | private size: number;
27 | private findPosition: FindPosition;
28 | private liveRate: number;
29 | private allowRotate: boolean;
30 |
31 | private totalSquares: number = 0;
32 | private maxHeight: number = -1;
33 | private maxWidth: number = -1;
34 |
35 | private randomDots: IPoint[] = [];
36 | private bestDot: IPoint;
37 | get minHeight() {
38 | return this.totalSquares / this.maxWidth;
39 | }
40 | get minWidth() {
41 | return this.totalSquares / this.maxHeight;
42 | }
43 | constructor(rects: Rect[], options?: IGeneticOptions) {
44 | if (!options) {
45 | options = {} as IGeneticOptions;
46 | }
47 | this.rects = rects;
48 | this.size = options.size < 20 ? 20 : options.size;
49 | this.lifeTimes = options.lifeTimes || 8;
50 | this.liveRate = options.liveRate || 0.5;
51 | if (this.liveRate < 0 || this.liveRate > 1) {
52 | this.liveRate = 0.5;
53 | }
54 | this.findPosition = options.findPosition;
55 | this.allowRotate = options.allowRotate === true ? true : false;
56 | }
57 | public calc(): IPoint {
58 | this.open();
59 | this.work();
60 | return this.close();
61 | }
62 | private open() {
63 | // 二维空间内寻找边界
64 | for (const rect of this.rects) {
65 | this.totalSquares = rect.height * rect.width + this.totalSquares;
66 | this.maxHeight = rect.height + this.maxHeight;
67 | this.maxWidth = rect.width + this.maxWidth;
68 | }
69 | // 二维空间内生成随机点 虽然无法保证分布是随机分布,但是对遗传算法来说无所谓了。
70 | for (let i = 0; i < this.size; i++) {
71 | const randomHeight = this.maxHeight * Math.random() + this.minHeight;
72 | const randomWidth = this.maxWidth * Math.random() + this.minWidth;
73 | this.randomDots.push({
74 | x: randomWidth,
75 | y: randomHeight,
76 | });
77 | }
78 | }
79 | private work() {
80 | while (this.lifeTimes--) {
81 | const generation: ICalResult[] = [];
82 | for (const dot of this.randomDots) {
83 | // 生活
84 | const binPack = new MaxRectBinPack(dot.x, dot.y, this.allowRotate);
85 | const clonedRects = this.getRects();
86 | const result = binPack.insertRects(clonedRects, this.findPosition);
87 | generation.push({
88 | dot,
89 | fitAll: result.length === this.rects.length,
90 | occupancy: binPack.occupancy(),
91 | });
92 | }
93 | // 淘汰
94 | generation.sort((geneticA, geneticB) => {
95 | if (geneticB.fitAll && geneticA.fitAll) {
96 | return geneticB.occupancy - geneticA.occupancy;
97 | } else if (geneticB.fitAll) {
98 | return 1;
99 | } else if (geneticA.fitAll) {
100 | return -1;
101 | } else {
102 | return geneticB.occupancy - geneticA.occupancy;
103 | }
104 | });
105 | this.bestDot = generation[0].dot;
106 | // 后置位淘汰有利于数据优化
107 | if (generation.length > this.size) {
108 | generation.splice(this.size, generation.length - this.size);
109 | }
110 | const killerStart = Math.ceil(this.liveRate * generation.length);
111 | generation.splice(killerStart, generation.length - killerStart);
112 | for (let i = generation.length - 1; i > 0; i--) {
113 | if (!generation[i].fitAll) {
114 | generation.splice(i, 1);
115 | }
116 | }
117 | this.randomDots = [];
118 | // 新生
119 | if (generation.length === 0 || generation.length === 1) {
120 | // 如果团灭了 或者 无法继续交配
121 | for (let i = 0; i < this.size; i++) {
122 | const randomHeight = this.maxHeight * Math.random() + this.minHeight;
123 | const randomWidth = this.maxWidth * Math.random() + this.minWidth;
124 | this.randomDots.push({
125 | x: randomWidth,
126 | y: randomHeight,
127 | });
128 | }
129 | } else {
130 | // 非随机交配
131 | for (let i = 0; i < generation.length; i++) {
132 | this.randomDots.push(generation[i].dot);
133 | for (let j = i + 1; j < generation.length; j++) {
134 | const startPoint = generation[i].dot;
135 | const endPoint = generation[j].dot;
136 | const [sonX, daughterX] = this.cross(startPoint.x, endPoint.x);
137 | const [sonY, daughterY] = this.cross(startPoint.y, endPoint.y);
138 | this.randomDots.push({
139 | x: sonX,
140 | y: sonY,
141 | });
142 | this.randomDots.push({
143 | x: daughterX,
144 | y: daughterY,
145 | });
146 | }
147 | }
148 | // 基因突变
149 | for (let i = 0; i < 20; i++) {
150 | const randomHeight = this.maxHeight * Math.random() + this.minHeight;
151 | const randomWidth = this.maxWidth * Math.random() + this.minWidth;
152 | this.randomDots.push({
153 | x: randomWidth,
154 | y: randomHeight,
155 | });
156 | }
157 | }
158 | }
159 | }
160 | private close() {
161 | return this.bestDot;
162 | }
163 | private getRects(): Rect[] {
164 | return this.rects.map(i => i.clone()); // tslint:disable-line arrow-parens
165 | }
166 | private cross(x: number, y: number) {
167 | const lerp = (a: number, b: number) => {
168 | return a + (b - a);
169 | };
170 | const formX = parseInt(x * 100 + '', 10);
171 | const formY = parseInt(y * 100 + '', 10);
172 | const binX = formX
173 | .toString(2)
174 | .split('')
175 | .map($ => parseInt($, 10)); // tslint:disable-line
176 | const binY = formY
177 | .toString(2)
178 | .split('')
179 | .map($ => parseInt($, 10)); // tslint:disable-line
180 |
181 | const son = [].concat(binX);
182 | const daughter = [].concat(binY);
183 | const i = Math.floor(Math.random() * binY.length);
184 | son[i] = lerp(binX[i], binY[i]);
185 | daughter[i] = lerp(binY[i], binX[i]);
186 | const sonvalue = parseInt(son.join(''), 2) / 100;
187 | const daughtervalue = parseInt(daughter.join(''), 2) / 100;
188 | return [sonvalue, daughtervalue];
189 | }
190 | }
191 |
192 | export default function(rect: Rect[], options: IGeneticOptions) {
193 | const g = new Genetic(rect, options);
194 | return g.calc();
195 | }
196 |
--------------------------------------------------------------------------------
/test/rects1.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "x": 0,
4 | "y": 0,
5 | "width": 387,
6 | "height": 360,
7 | "isRotated": false,
8 | "info": { "name": "tileset-ornaments-fCap-bear" }
9 | },
10 | {
11 | "x": 0,
12 | "y": 0,
13 | "width": 331,
14 | "height": 257,
15 | "isRotated": false,
16 | "info": { "name": "tileset-ornaments-fCap-cook" }
17 | },
18 | {
19 | "x": 0,
20 | "y": 0,
21 | "width": 288,
22 | "height": 222,
23 | "isRotated": false,
24 | "info": { "name": "tileset-ornaments-fCap-huabei" }
25 | },
26 | {
27 | "x": 0,
28 | "y": 0,
29 | "width": 360,
30 | "height": 225,
31 | "isRotated": false,
32 | "info": { "name": "tileset-ornaments-fCap-lady" }
33 | },
34 | {
35 | "x": 0,
36 | "y": 0,
37 | "width": 370,
38 | "height": 252,
39 | "isRotated": false,
40 | "info": { "name": "tileset-ornaments-fCap-lili" }
41 | },
42 | {
43 | "x": 0,
44 | "y": 0,
45 | "width": 356,
46 | "height": 231,
47 | "isRotated": false,
48 | "info": { "name": "tileset-ornaments-fCap-mike" }
49 | },
50 | {
51 | "x": 0,
52 | "y": 0,
53 | "width": 352,
54 | "height": 211,
55 | "isRotated": false,
56 | "info": { "name": "tileset-ornaments-fCap-rich" }
57 | },
58 | {
59 | "x": 0,
60 | "y": 0,
61 | "width": 315,
62 | "height": 260,
63 | "isRotated": false,
64 | "info": { "name": "tileset-ornaments-fCap-spring-festival" }
65 | },
66 | {
67 | "x": 0,
68 | "y": 0,
69 | "width": 307,
70 | "height": 258,
71 | "isRotated": false,
72 | "info": { "name": "tileset-ornaments-fCap-wild" }
73 | },
74 | {
75 | "x": 0,
76 | "y": 0,
77 | "width": 369,
78 | "height": 267,
79 | "isRotated": false,
80 | "info": { "name": "tileset-ornaments-fCoat-bear" }
81 | },
82 | {
83 | "x": 0,
84 | "y": 0,
85 | "width": 379,
86 | "height": 285,
87 | "isRotated": false,
88 | "info": { "name": "tileset-ornaments-fCoat-cook" }
89 | },
90 | {
91 | "x": 0,
92 | "y": 0,
93 | "width": 381,
94 | "height": 277,
95 | "isRotated": false,
96 | "info": { "name": "tileset-ornaments-fCoat-huabei" }
97 | },
98 | {
99 | "x": 0,
100 | "y": 0,
101 | "width": 381,
102 | "height": 293,
103 | "isRotated": false,
104 | "info": { "name": "tileset-ornaments-fCoat-lady" }
105 | },
106 | {
107 | "x": 0,
108 | "y": 0,
109 | "width": 382,
110 | "height": 264,
111 | "isRotated": false,
112 | "info": { "name": "tileset-ornaments-fCoat-lili" }
113 | },
114 | {
115 | "x": 0,
116 | "y": 0,
117 | "width": 400,
118 | "height": 274,
119 | "isRotated": false,
120 | "info": { "name": "tileset-ornaments-fCoat-mike" }
121 | },
122 | {
123 | "x": 0,
124 | "y": 0,
125 | "width": 375,
126 | "height": 229,
127 | "isRotated": false,
128 | "info": { "name": "tileset-ornaments-fCoat-rich" }
129 | },
130 | {
131 | "x": 0,
132 | "y": 0,
133 | "width": 381,
134 | "height": 263,
135 | "isRotated": false,
136 | "info": { "name": "tileset-ornaments-fCoat-wild" }
137 | },
138 | {
139 | "x": 0,
140 | "y": 0,
141 | "width": 252,
142 | "height": 169,
143 | "isRotated": false,
144 | "info": { "name": "tileset-ornaments-fGlass-bear" }
145 | },
146 | {
147 | "x": 0,
148 | "y": 0,
149 | "width": 217,
150 | "height": 235,
151 | "isRotated": false,
152 | "info": { "name": "tileset-ornaments-fGlass-mike" }
153 | },
154 | {
155 | "x": 0,
156 | "y": 0,
157 | "width": 298,
158 | "height": 187,
159 | "isRotated": false,
160 | "info": { "name": "tileset-ornaments-fGlass-rich" }
161 | },
162 | {
163 | "x": 0,
164 | "y": 0,
165 | "width": 342,
166 | "height": 209,
167 | "isRotated": false,
168 | "info": { "name": "tileset-ornaments-fGlass-wild" }
169 | },
170 | {
171 | "x": 0,
172 | "y": 0,
173 | "width": 386,
174 | "height": 242,
175 | "isRotated": false,
176 | "info": { "name": "tileset-ornaments-fNecklace-cook" }
177 | },
178 | {
179 | "x": 0,
180 | "y": 0,
181 | "width": 364,
182 | "height": 272,
183 | "isRotated": false,
184 | "info": { "name": "tileset-ornaments-fNecklace-lili" }
185 | },
186 | {
187 | "x": 0,
188 | "y": 0,
189 | "width": 361,
190 | "height": 296,
191 | "isRotated": false,
192 | "info": { "name": "tileset-ornaments-fNecklace-rich" }
193 | },
194 | {
195 | "x": 0,
196 | "y": 0,
197 | "width": 406,
198 | "height": 255,
199 | "isRotated": false,
200 | "info": { "name": "tileset-ornaments-fNecklace-spring-festival" }
201 | },
202 | {
203 | "x": 0,
204 | "y": 0,
205 | "width": 130,
206 | "height": 131,
207 | "isRotated": false,
208 | "info": { "name": "tileset-ornaments-fSlag-apple" }
209 | },
210 | {
211 | "x": 0,
212 | "y": 0,
213 | "width": 130,
214 | "height": 131,
215 | "isRotated": false,
216 | "info": { "name": "tileset-ornaments-fSlag-bunch" }
217 | },
218 | {
219 | "x": 0,
220 | "y": 0,
221 | "width": 130,
222 | "height": 131,
223 | "isRotated": false,
224 | "info": { "name": "tileset-ornaments-fSlag-donut" }
225 | },
226 | {
227 | "x": 0,
228 | "y": 0,
229 | "width": 130,
230 | "height": 131,
231 | "isRotated": false,
232 | "info": { "name": "tileset-ornaments-fSlag-grape" }
233 | },
234 | {
235 | "x": 0,
236 | "y": 0,
237 | "width": 130,
238 | "height": 131,
239 | "isRotated": false,
240 | "info": { "name": "tileset-ornaments-fSlag-icecream" }
241 | },
242 | {
243 | "x": 0,
244 | "y": 0,
245 | "width": 339,
246 | "height": 362,
247 | "isRotated": false,
248 | "info": { "name": "tileset-ornaments-sCap-bear" }
249 | },
250 | {
251 | "x": 0,
252 | "y": 0,
253 | "width": 323,
254 | "height": 235,
255 | "isRotated": false,
256 | "info": { "name": "tileset-ornaments-sCap-cook" }
257 | },
258 | {
259 | "x": 0,
260 | "y": 0,
261 | "width": 331,
262 | "height": 227,
263 | "isRotated": false,
264 | "info": { "name": "tileset-ornaments-sCap-huabei" }
265 | },
266 | {
267 | "x": 0,
268 | "y": 0,
269 | "width": 359,
270 | "height": 227,
271 | "isRotated": false,
272 | "info": { "name": "tileset-ornaments-sCap-lady" }
273 | },
274 | {
275 | "x": 0,
276 | "y": 0,
277 | "width": 381,
278 | "height": 235,
279 | "isRotated": false,
280 | "info": { "name": "tileset-ornaments-sCap-lili" }
281 | },
282 | {
283 | "x": 0,
284 | "y": 0,
285 | "width": 327,
286 | "height": 227,
287 | "isRotated": false,
288 | "info": { "name": "tileset-ornaments-sCap-mike" }
289 | },
290 | {
291 | "x": 0,
292 | "y": 0,
293 | "width": 352,
294 | "height": 214,
295 | "isRotated": false,
296 | "info": { "name": "tileset-ornaments-sCap-rich" }
297 | },
298 | {
299 | "x": 0,
300 | "y": 0,
301 | "width": 315,
302 | "height": 256,
303 | "isRotated": false,
304 | "info": { "name": "tileset-ornaments-sCap-spring-festival" }
305 | },
306 | {
307 | "x": 0,
308 | "y": 0,
309 | "width": 305,
310 | "height": 264,
311 | "isRotated": false,
312 | "info": { "name": "tileset-ornaments-sCap-wild" }
313 | },
314 | {
315 | "x": 0,
316 | "y": 0,
317 | "width": 387,
318 | "height": 266,
319 | "isRotated": false,
320 | "info": { "name": "tileset-ornaments-sCoat-bear" }
321 | },
322 | {
323 | "x": 0,
324 | "y": 0,
325 | "width": 385,
326 | "height": 282,
327 | "isRotated": false,
328 | "info": { "name": "tileset-ornaments-sCoat-cook" }
329 | },
330 | {
331 | "x": 0,
332 | "y": 0,
333 | "width": 384,
334 | "height": 252,
335 | "isRotated": false,
336 | "info": { "name": "tileset-ornaments-sCoat-huabei" }
337 | },
338 | {
339 | "x": 0,
340 | "y": 0,
341 | "width": 394,
342 | "height": 270,
343 | "isRotated": false,
344 | "info": { "name": "tileset-ornaments-sCoat-lady" }
345 | },
346 | {
347 | "x": 0,
348 | "y": 0,
349 | "width": 386,
350 | "height": 254,
351 | "isRotated": false,
352 | "info": { "name": "tileset-ornaments-sCoat-lili" }
353 | },
354 | {
355 | "x": 0,
356 | "y": 0,
357 | "width": 386,
358 | "height": 250,
359 | "isRotated": false,
360 | "info": { "name": "tileset-ornaments-sCoat-mike" }
361 | },
362 | {
363 | "x": 0,
364 | "y": 0,
365 | "width": 382,
366 | "height": 227,
367 | "isRotated": false,
368 | "info": { "name": "tileset-ornaments-sCoat-rich" }
369 | },
370 | {
371 | "x": 0,
372 | "y": 0,
373 | "width": 397,
374 | "height": 262,
375 | "isRotated": false,
376 | "info": { "name": "tileset-ornaments-sCoat-wild" }
377 | },
378 | {
379 | "x": 0,
380 | "y": 0,
381 | "width": 428,
382 | "height": 508,
383 | "isRotated": false,
384 | "info": { "name": "tileset-ornaments-sets-bear" }
385 | },
386 | {
387 | "x": 0,
388 | "y": 0,
389 | "width": 422,
390 | "height": 523,
391 | "isRotated": false,
392 | "info": { "name": "tileset-ornaments-sets-cook" }
393 | },
394 | {
395 | "x": 0,
396 | "y": 0,
397 | "width": 423,
398 | "height": 498,
399 | "isRotated": false,
400 | "info": { "name": "tileset-ornaments-sets-huabei" }
401 | },
402 | {
403 | "x": 0,
404 | "y": 0,
405 | "width": 422,
406 | "height": 489,
407 | "isRotated": false,
408 | "info": { "name": "tileset-ornaments-sets-lady" }
409 | },
410 | {
411 | "x": 0,
412 | "y": 0,
413 | "width": 430,
414 | "height": 493,
415 | "isRotated": false,
416 | "info": { "name": "tileset-ornaments-sets-lili" }
417 | },
418 | {
419 | "x": 0,
420 | "y": 0,
421 | "width": 423,
422 | "height": 490,
423 | "isRotated": false,
424 | "info": { "name": "tileset-ornaments-sets-mike" }
425 | },
426 | {
427 | "x": 0,
428 | "y": 0,
429 | "width": 422,
430 | "height": 497,
431 | "isRotated": false,
432 | "info": { "name": "tileset-ornaments-sets-rich" }
433 | },
434 | {
435 | "x": 0,
436 | "y": 0,
437 | "width": 427,
438 | "height": 517,
439 | "isRotated": false,
440 | "info": { "name": "tileset-ornaments-sets-spring-festival" }
441 | },
442 | {
443 | "x": 0,
444 | "y": 0,
445 | "width": 428,
446 | "height": 532,
447 | "isRotated": false,
448 | "info": { "name": "tileset-ornaments-sets-wild" }
449 | },
450 | {
451 | "x": 0,
452 | "y": 0,
453 | "width": 232,
454 | "height": 175,
455 | "isRotated": false,
456 | "info": { "name": "tileset-ornaments-sGlass-rich" }
457 | },
458 | {
459 | "x": 0,
460 | "y": 0,
461 | "width": 206,
462 | "height": 214,
463 | "isRotated": false,
464 | "info": { "name": "tileset-ornaments-snacks-apple1" }
465 | },
466 | {
467 | "x": 0,
468 | "y": 0,
469 | "width": 206,
470 | "height": 214,
471 | "isRotated": false,
472 | "info": { "name": "tileset-ornaments-snacks-apple2" }
473 | },
474 | {
475 | "x": 0,
476 | "y": 0,
477 | "width": 206,
478 | "height": 214,
479 | "isRotated": false,
480 | "info": { "name": "tileset-ornaments-snacks-apple3" }
481 | },
482 | {
483 | "x": 0,
484 | "y": 0,
485 | "width": 190,
486 | "height": 248,
487 | "isRotated": false,
488 | "info": { "name": "tileset-ornaments-snacks-bunch1" }
489 | },
490 | {
491 | "x": 0,
492 | "y": 0,
493 | "width": 190,
494 | "height": 248,
495 | "isRotated": false,
496 | "info": { "name": "tileset-ornaments-snacks-bunch2" }
497 | },
498 | {
499 | "x": 0,
500 | "y": 0,
501 | "width": 190,
502 | "height": 248,
503 | "isRotated": false,
504 | "info": { "name": "tileset-ornaments-snacks-bunch3" }
505 | },
506 | {
507 | "x": 0,
508 | "y": 0,
509 | "width": 224,
510 | "height": 222,
511 | "isRotated": false,
512 | "info": { "name": "tileset-ornaments-snacks-donut1" }
513 | },
514 | {
515 | "x": 0,
516 | "y": 0,
517 | "width": 224,
518 | "height": 222,
519 | "isRotated": false,
520 | "info": { "name": "tileset-ornaments-snacks-donut2" }
521 | },
522 | {
523 | "x": 0,
524 | "y": 0,
525 | "width": 224,
526 | "height": 222,
527 | "isRotated": false,
528 | "info": { "name": "tileset-ornaments-snacks-donut3" }
529 | },
530 | {
531 | "x": 0,
532 | "y": 0,
533 | "width": 206,
534 | "height": 217,
535 | "isRotated": false,
536 | "info": { "name": "tileset-ornaments-snacks-grape1" }
537 | },
538 | {
539 | "x": 0,
540 | "y": 0,
541 | "width": 206,
542 | "height": 217,
543 | "isRotated": false,
544 | "info": { "name": "tileset-ornaments-snacks-grape2" }
545 | },
546 | {
547 | "x": 0,
548 | "y": 0,
549 | "width": 206,
550 | "height": 217,
551 | "isRotated": false,
552 | "info": { "name": "tileset-ornaments-snacks-grape3" }
553 | },
554 | {
555 | "x": 0,
556 | "y": 0,
557 | "width": 206,
558 | "height": 237,
559 | "isRotated": false,
560 | "info": { "name": "tileset-ornaments-snacks-icecream1" }
561 | },
562 | {
563 | "x": 0,
564 | "y": 0,
565 | "width": 206,
566 | "height": 237,
567 | "isRotated": false,
568 | "info": { "name": "tileset-ornaments-snacks-icecream2" }
569 | },
570 | {
571 | "x": 0,
572 | "y": 0,
573 | "width": 206,
574 | "height": 237,
575 | "isRotated": false,
576 | "info": { "name": "tileset-ornaments-snacks-icecream3" }
577 | },
578 | {
579 | "x": 0,
580 | "y": 0,
581 | "width": 381,
582 | "height": 226,
583 | "isRotated": false,
584 | "info": { "name": "tileset-ornaments-sNecklace-cook" }
585 | },
586 | {
587 | "x": 0,
588 | "y": 0,
589 | "width": 356,
590 | "height": 289,
591 | "isRotated": false,
592 | "info": { "name": "tileset-ornaments-sNecklace-rich" }
593 | },
594 | {
595 | "x": 0,
596 | "y": 0,
597 | "width": 397,
598 | "height": 211,
599 | "isRotated": false,
600 | "info": { "name": "tileset-ornaments-sNecklace-spring-festival" }
601 | }
602 | ]
603 |
--------------------------------------------------------------------------------
/src/max-rect-bin-pack.ts:
--------------------------------------------------------------------------------
1 | import Rect from './rect';
2 |
3 | export enum FindPosition {
4 | ShortSideFit = 0,
5 | BottomLeft,
6 | ContactPoint,
7 | LongSideFit,
8 | AreaFit,
9 | }
10 |
11 | // 强制引用传递套个壳子
12 | interface IScoreCounter {
13 | value: number;
14 | }
15 |
16 | class MaxRectBinPack {
17 | private containerHeight: number;
18 | private containerWidth: number;
19 | private allowRotate: boolean;
20 | private freeRects: Rect[] = [];
21 | private usedRects: Rect[] = [];
22 | /**
23 | * 构建方程
24 | * @param width {number} 画板宽度
25 | * @param height {number} 画板高度
26 | * @param allowRotate {boolean} 允许旋转
27 | */
28 | constructor(width: number, height: number, allowRotate?: boolean) {
29 | this.containerHeight = height;
30 | this.containerWidth = width;
31 | this.allowRotate = allowRotate === true;
32 |
33 | const rect = new Rect();
34 | rect.x = 0;
35 | rect.y = 0;
36 | rect.width = width;
37 | rect.height = height;
38 |
39 | this.freeRects.push(rect);
40 | }
41 | /**
42 | * 在线算法入口 插入矩形方法
43 | * @param width {number}
44 | * @param height {number}
45 | * @param method {FindPosition}
46 | */
47 | public insert(width: number, height: number, method: FindPosition): Rect {
48 | // width height 参数合法性检查
49 | if (width <= 0 || height <= 0) {
50 | throw new Error(
51 | `width & height should greater than 0, but got width as ${width}, height as ${height}`,
52 | );
53 | }
54 | // method 合法性检查
55 | if (method <= FindPosition.ShortSideFit || method >= FindPosition.AreaFit) {
56 | method = FindPosition.ShortSideFit;
57 | }
58 |
59 | let newRect = new Rect();
60 |
61 | const score1: IScoreCounter = {
62 | value: 0,
63 | };
64 |
65 | const score2: IScoreCounter = {
66 | value: 0,
67 | };
68 |
69 | switch (method) {
70 | case FindPosition.ShortSideFit:
71 | newRect = this.findPositionForNewNodeBestShortSideFit(
72 | width,
73 | height,
74 | score1,
75 | score2,
76 | );
77 | break;
78 | case FindPosition.BottomLeft:
79 | newRect = this.findPositionForNewNodeBottomLeft(
80 | width,
81 | height,
82 | score1,
83 | score2,
84 | );
85 | break;
86 | case FindPosition.ContactPoint:
87 | newRect = this.findPositionForNewNodeContactPoint(
88 | width,
89 | height,
90 | score1,
91 | );
92 | break;
93 | case FindPosition.LongSideFit:
94 | newRect = this.findPositionForNewNodeBestLongSideFit(
95 | width,
96 | height,
97 | score2,
98 | score1,
99 | );
100 | break;
101 | case FindPosition.AreaFit:
102 | newRect = this.findPositionForNewNodeBestAreaFit(
103 | width,
104 | height,
105 | score1,
106 | score2,
107 | );
108 | break;
109 | }
110 |
111 | if (newRect.height === 0) {
112 | return newRect;
113 | }
114 | if (this.allowRotate) { // 更新旋转属性
115 | if (newRect.height === height && newRect.width === width) {
116 | newRect.isRotated = false;
117 | } else {
118 | // TODO: check is really rotated
119 | newRect.isRotated = true;
120 | }
121 | }
122 | this.placeRectangle(newRect);
123 | return newRect;
124 | }
125 | /**
126 | * 算法离线入口 插入一组举行
127 | * @param rects {Rect[]} 矩形数组
128 | * @param method {FindPosition} 查找位置的方法
129 | */
130 | public insertRects(rects: Rect[], method: FindPosition): Rect[] {
131 | // rects 参数合法性检查
132 | if (rects && rects.length === 0) {
133 | throw new Error('rects should be array with length greater than zero');
134 | }
135 | // method 合法性检查
136 | if (method <= FindPosition.ShortSideFit || method >= FindPosition.AreaFit) {
137 | method = FindPosition.ShortSideFit;
138 | }
139 |
140 | const result: Rect[] = [];
141 | while (rects.length > 0) {
142 | const bestScore1: IScoreCounter = {
143 | value: Infinity,
144 | };
145 | const bestScore2: IScoreCounter = {
146 | value: Infinity,
147 | };
148 | let bestRectIndex = -1;
149 | let bestNode: Rect;
150 |
151 | for (let i = 0; i < rects.length; ++i) {
152 | const score1: IScoreCounter = {
153 | value: 0,
154 | };
155 | const score2: IScoreCounter = {
156 | value: 0,
157 | };
158 | const newNode: Rect = this.scoreRectangle(
159 | rects[i].width,
160 | rects[i].height,
161 | method,
162 | score1,
163 | score2,
164 | );
165 |
166 | if (
167 | score1.value < bestScore1.value ||
168 | (score1.value === bestScore1.value && score2.value < bestScore2.value)
169 | ) {
170 | bestScore1.value = score1.value;
171 | bestScore2.value = score2.value;
172 | bestNode = newNode;
173 | bestRectIndex = i;
174 | }
175 | }
176 |
177 | if (bestRectIndex === -1) {
178 | return result;
179 | }
180 | this.placeRectangle(bestNode);
181 |
182 | bestNode.info = rects[bestRectIndex].info;
183 | if (this.allowRotate) {
184 | if (
185 | bestNode.height === rects[bestRectIndex].height &&
186 | bestNode.width === rects[bestRectIndex].width
187 | ) {
188 | bestNode.isRotated = false;
189 | } else {
190 | bestNode.isRotated = true;
191 | }
192 | }
193 |
194 | rects.splice(bestRectIndex, 1);
195 |
196 | result.push(bestNode);
197 | }
198 | return result;
199 | }
200 | public occupancy(): number {
201 | let usedSurfaceArea = 0;
202 | for (const rect of this.usedRects) {
203 | usedSurfaceArea += rect.width * rect.height;
204 | }
205 | return usedSurfaceArea / (this.containerWidth * this.containerHeight);
206 | }
207 | /**
208 | *
209 | * @param node
210 | */
211 | private placeRectangle(node: Rect) {
212 | let numRectanglesToProcess = this.freeRects.length;
213 | for (let i = 0; i < numRectanglesToProcess; i++) {
214 | if (this.splitFreeNode(this.freeRects[i], node)) {
215 | this.freeRects.splice(i, 1);
216 | i--;
217 | numRectanglesToProcess--;
218 | }
219 | }
220 |
221 | this.pruneFreeList();
222 | this.usedRects.push(node);
223 | }
224 | private scoreRectangle(
225 | width: number,
226 | height: number,
227 | method: FindPosition,
228 | score1: IScoreCounter,
229 | score2: IScoreCounter,
230 | ): Rect {
231 | let newNode = new Rect();
232 | score1.value = Infinity;
233 | score2.value = Infinity;
234 | switch (method) {
235 | case FindPosition.ShortSideFit:
236 | newNode = this.findPositionForNewNodeBestShortSideFit(
237 | width,
238 | height,
239 | score1,
240 | score2,
241 | );
242 | break;
243 | case FindPosition.BottomLeft:
244 | newNode = this.findPositionForNewNodeBottomLeft(
245 | width,
246 | height,
247 | score1,
248 | score2,
249 | );
250 | break;
251 | case FindPosition.ContactPoint:
252 | newNode = this.findPositionForNewNodeContactPoint(
253 | width,
254 | height,
255 | score1,
256 | );
257 | // todo: reverse
258 | score1.value = -score1.value; // Reverse since we are minimizing, but for contact point score bigger is better.
259 | break;
260 | case FindPosition.LongSideFit:
261 | newNode = this.findPositionForNewNodeBestLongSideFit(
262 | width,
263 | height,
264 | score2,
265 | score1,
266 | );
267 | break;
268 | case FindPosition.AreaFit:
269 | newNode = this.findPositionForNewNodeBestAreaFit(
270 | width,
271 | height,
272 | score1,
273 | score2,
274 | );
275 | break;
276 | }
277 |
278 | // Cannot fit the current Rectangle.
279 | if (newNode.height === 0) {
280 | score1.value = Infinity;
281 | score2.value = Infinity;
282 | }
283 |
284 | return newNode;
285 | }
286 | private findPositionForNewNodeBottomLeft(
287 | width: number,
288 | height: number,
289 | bestY: IScoreCounter,
290 | bestX: IScoreCounter,
291 | ): Rect {
292 | const freeRects = this.freeRects;
293 | const bestNode = new Rect();
294 |
295 | bestY.value = Infinity;
296 | let topSideY;
297 | for (const rect of this.freeRects) {
298 | // Try to place the Rectangle in upright (non-flipped) orientation.
299 | if (rect.width >= width && rect.height >= height) {
300 | topSideY = rect.y + height;
301 | if (
302 | topSideY < bestY.value ||
303 | (topSideY === bestY.value && rect.x < bestX.value)
304 | ) {
305 | bestNode.x = rect.x;
306 | bestNode.y = rect.y;
307 | bestNode.width = width;
308 | bestNode.height = height;
309 | bestY.value = topSideY;
310 | bestX.value = rect.x;
311 | }
312 | }
313 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
314 | topSideY = rect.y + width;
315 | if (
316 | topSideY < bestY.value ||
317 | (topSideY === bestY.value && rect.x < bestX.value)
318 | ) {
319 | bestNode.x = rect.x;
320 | bestNode.y = rect.y;
321 | bestNode.width = height;
322 | bestNode.height = width;
323 | bestY.value = topSideY;
324 | bestX.value = rect.x;
325 | }
326 | }
327 | }
328 | return bestNode;
329 | }
330 | private findPositionForNewNodeBestShortSideFit(
331 | width: number,
332 | height: number,
333 | bestShortSideFit: IScoreCounter,
334 | bestLongSideFit: IScoreCounter,
335 | ): Rect {
336 | const bestNode = new Rect();
337 | bestShortSideFit.value = Infinity;
338 |
339 | let leftoverHoriz;
340 | let leftoverVert;
341 | let shortSideFit;
342 | let longSideFit;
343 |
344 | for (const rect of this.freeRects) {
345 | // Try to place the Rectangle in upright (non-flipped) orientation.
346 | if (rect.width >= width && rect.height >= height) {
347 | leftoverHoriz = Math.abs(rect.width - width);
348 | leftoverVert = Math.abs(rect.height - height);
349 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
350 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
351 |
352 | if (
353 | shortSideFit < bestShortSideFit.value ||
354 | (shortSideFit === bestShortSideFit.value &&
355 | longSideFit < bestLongSideFit.value)
356 | ) {
357 | bestNode.x = rect.x;
358 | bestNode.y = rect.y;
359 | bestNode.width = width;
360 | bestNode.height = height;
361 | bestShortSideFit.value = shortSideFit;
362 | bestLongSideFit.value = longSideFit;
363 | }
364 | }
365 | let flippedLeftoverHoriz;
366 | let flippedLeftoverVert;
367 | let flippedShortSideFit;
368 | let flippedLongSideFit;
369 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
370 | flippedLeftoverHoriz = Math.abs(rect.width - height);
371 | flippedLeftoverVert = Math.abs(rect.height - width);
372 | flippedShortSideFit = Math.min(
373 | flippedLeftoverHoriz,
374 | flippedLeftoverVert,
375 | );
376 | flippedLongSideFit = Math.max(
377 | flippedLeftoverHoriz,
378 | flippedLeftoverVert,
379 | );
380 |
381 | if (
382 | flippedShortSideFit < bestShortSideFit.value ||
383 | (flippedShortSideFit === bestShortSideFit.value &&
384 | flippedLongSideFit < bestLongSideFit.value)
385 | ) {
386 | bestNode.x = rect.x;
387 | bestNode.y = rect.y;
388 | bestNode.width = height;
389 | bestNode.height = width;
390 | bestShortSideFit.value = flippedShortSideFit;
391 | bestLongSideFit.value = flippedLongSideFit;
392 | }
393 | }
394 | }
395 |
396 | return bestNode;
397 | }
398 | private findPositionForNewNodeBestLongSideFit(
399 | width: number,
400 | height: number,
401 | bestShortSideFit: IScoreCounter,
402 | bestLongSideFit: IScoreCounter,
403 | ): Rect {
404 | const bestNode = new Rect();
405 | bestLongSideFit.value = Infinity;
406 |
407 | let leftoverHoriz;
408 | let leftoverVert;
409 | let shortSideFit;
410 | let longSideFit;
411 | for (const rect of this.freeRects) {
412 | // Try to place the Rectangle in upright (non-flipped) orientation.
413 | if (rect.width >= width && rect.height >= height) {
414 | leftoverHoriz = Math.abs(rect.width - width);
415 | leftoverVert = Math.abs(rect.height - height);
416 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
417 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
418 |
419 | if (
420 | longSideFit < bestLongSideFit.value ||
421 | (longSideFit === bestLongSideFit.value &&
422 | shortSideFit < bestShortSideFit.value)
423 | ) {
424 | bestNode.x = rect.x;
425 | bestNode.y = rect.y;
426 | bestNode.width = width;
427 | bestNode.height = height;
428 | bestShortSideFit.value = shortSideFit;
429 | bestLongSideFit.value = longSideFit;
430 | }
431 | }
432 |
433 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
434 | leftoverHoriz = Math.abs(rect.width - height);
435 | leftoverVert = Math.abs(rect.height - width);
436 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
437 | longSideFit = Math.max(leftoverHoriz, leftoverVert);
438 |
439 | if (
440 | longSideFit < bestLongSideFit.value ||
441 | (longSideFit === bestLongSideFit.value &&
442 | shortSideFit < bestShortSideFit.value)
443 | ) {
444 | bestNode.x = rect.x;
445 | bestNode.y = rect.y;
446 | bestNode.width = height;
447 | bestNode.height = width;
448 | bestShortSideFit.value = shortSideFit;
449 | bestLongSideFit.value = longSideFit;
450 | }
451 | }
452 | }
453 | return bestNode;
454 | }
455 | private findPositionForNewNodeBestAreaFit(
456 | width: number,
457 | height: number,
458 | bestAreaFit: IScoreCounter,
459 | bestShortSideFit: IScoreCounter,
460 | ): Rect {
461 | const bestNode = new Rect();
462 | bestAreaFit.value = Infinity;
463 |
464 | let leftoverHoriz;
465 | let leftoverVert;
466 | let shortSideFit;
467 | let areaFit;
468 |
469 | for (const rect of this.freeRects) {
470 | areaFit = rect.width * rect.height - width * height;
471 |
472 | // Try to place the Rectangle in upright (non-flipped) orientation.
473 | if (rect.width >= width && rect.height >= height) {
474 | leftoverHoriz = Math.abs(rect.width - width);
475 | leftoverVert = Math.abs(rect.height - height);
476 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
477 |
478 | if (
479 | areaFit < bestAreaFit.value ||
480 | (areaFit === bestAreaFit.value &&
481 | shortSideFit < bestShortSideFit.value)
482 | ) {
483 | bestNode.x = rect.x;
484 | bestNode.y = rect.y;
485 | bestNode.width = width;
486 | bestNode.height = height;
487 | bestShortSideFit.value = shortSideFit;
488 | bestAreaFit.value = areaFit;
489 | }
490 | }
491 |
492 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
493 | leftoverHoriz = Math.abs(rect.width - height);
494 | leftoverVert = Math.abs(rect.height - width);
495 | shortSideFit = Math.min(leftoverHoriz, leftoverVert);
496 |
497 | if (
498 | areaFit < bestAreaFit.value ||
499 | (areaFit === bestAreaFit.value &&
500 | shortSideFit < bestShortSideFit.value)
501 | ) {
502 | bestNode.x = rect.x;
503 | bestNode.y = rect.y;
504 | bestNode.width = height;
505 | bestNode.height = width;
506 | bestShortSideFit.value = shortSideFit;
507 | bestAreaFit.value = areaFit;
508 | }
509 | }
510 | }
511 | return bestNode;
512 | }
513 | private commonIntervalLength(
514 | i1start: number,
515 | i1end: number,
516 | i2start: number,
517 | i2end: number,
518 | ): number {
519 | if (i1end < i2start || i2end < i1start) {
520 | return 0;
521 | }
522 | return Math.min(i1end, i2end) - Math.max(i1start, i2start);
523 | }
524 | private contactPointScoreNode(
525 | x: number,
526 | y: number,
527 | width: number,
528 | height: number,
529 | ): number {
530 | let score = 0;
531 | if (x === 0 || x + width === this.containerWidth) {
532 | score += height;
533 | }
534 | if (y === 0 || y + height === this.containerHeight) {
535 | score += width;
536 | }
537 | for (const rect of this.usedRects) {
538 | if (rect.x === x + width || rect.x + rect.width === x) {
539 | score += this.commonIntervalLength(
540 | rect.y,
541 | rect.y + rect.height,
542 | y,
543 | y + height,
544 | );
545 | }
546 | if (rect.y === y + height || rect.y + rect.height === y) {
547 | score += this.commonIntervalLength(
548 | rect.x,
549 | rect.x + rect.width,
550 | x,
551 | x + width,
552 | );
553 | }
554 | }
555 | return score;
556 | }
557 | private findPositionForNewNodeContactPoint(
558 | width: number,
559 | height: number,
560 | bestContactScore: IScoreCounter,
561 | ): Rect {
562 | const bestNode = new Rect();
563 | bestContactScore.value = -1;
564 |
565 | let score;
566 | for (const rect of this.freeRects) {
567 | // Try to place the Rectangle in upright (non-flipped) orientation.
568 | if (rect.width >= width && rect.height >= height) {
569 | score = this.contactPointScoreNode(rect.x, rect.y, width, height);
570 | if (score > bestContactScore.value) {
571 | bestNode.x = rect.x;
572 | bestNode.y = rect.y;
573 | bestNode.width = width;
574 | bestNode.height = height;
575 | bestContactScore.value = score;
576 | }
577 | }
578 | if (this.allowRotate && rect.width >= height && rect.height >= width) {
579 | score = this.contactPointScoreNode(rect.x, rect.y, height, width);
580 | if (score > bestContactScore.value) {
581 | bestNode.x = rect.x;
582 | bestNode.y = rect.y;
583 | bestNode.width = height;
584 | bestNode.height = width;
585 | bestContactScore.value = score;
586 | }
587 | }
588 | }
589 | return bestNode;
590 | }
591 | private splitFreeNode(freeNode: Rect, usedNode: Rect): boolean {
592 | const freeRectangles = this.freeRects;
593 | // Test with SAT if the Rectangles even intersect.
594 | if (
595 | usedNode.x >= freeNode.x + freeNode.width ||
596 | usedNode.x + usedNode.width <= freeNode.x ||
597 | usedNode.y >= freeNode.y + freeNode.height ||
598 | usedNode.y + usedNode.height <= freeNode.y
599 | ) {
600 | return false;
601 | }
602 | let newNode;
603 | if (
604 | usedNode.x < freeNode.x + freeNode.width &&
605 | usedNode.x + usedNode.width > freeNode.x
606 | ) {
607 | // New node at the top side of the used node.
608 | if (
609 | usedNode.y > freeNode.y &&
610 | usedNode.y < freeNode.y + freeNode.height
611 | ) {
612 | newNode = freeNode.clone();
613 | newNode.height = usedNode.y - newNode.y;
614 | freeRectangles.push(newNode);
615 | }
616 |
617 | // New node at the bottom side of the used node.
618 | if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) {
619 | newNode = freeNode.clone();
620 | newNode.y = usedNode.y + usedNode.height;
621 | newNode.height =
622 | freeNode.y + freeNode.height - (usedNode.y + usedNode.height);
623 | freeRectangles.push(newNode);
624 | }
625 | }
626 |
627 | if (
628 | usedNode.y < freeNode.y + freeNode.height &&
629 | usedNode.y + usedNode.height > freeNode.y
630 | ) {
631 | // New node at the left side of the used node.
632 | if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) {
633 | newNode = freeNode.clone();
634 | newNode.width = usedNode.x - newNode.x;
635 | freeRectangles.push(newNode);
636 | }
637 |
638 | // New node at the right side of the used node.
639 | if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) {
640 | newNode = freeNode.clone();
641 | newNode.x = usedNode.x + usedNode.width;
642 | newNode.width =
643 | freeNode.x + freeNode.width - (usedNode.x + usedNode.width);
644 | freeRectangles.push(newNode);
645 | }
646 | }
647 | return true;
648 | }
649 | private pruneFreeList() {
650 | const freeRectangles = this.freeRects;
651 | for (let i = 0; i < freeRectangles.length; i++) {
652 | for (let j = i + 1; j < freeRectangles.length; j++) {
653 | if (freeRectangles[i].isIn(freeRectangles[j])) {
654 | freeRectangles.splice(i, 1);
655 | break;
656 | }
657 | if (freeRectangles[j].isIn(freeRectangles[i])) {
658 | freeRectangles.splice(j, 1);
659 | }
660 | }
661 | }
662 | }
663 | }
664 |
665 | export default MaxRectBinPack;
666 |
--------------------------------------------------------------------------------