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 |
|
9 | /**
|
10 | * Options for MaxRect Packer
|
11 | * @property {boolean} options.smart Smart sizing packer (default is true)
|
12 | * @property {boolean} options.pot use power of 2 sizing (default is true)
|
13 | * @property {boolean} options.square use square size (default is false)
|
14 | * @property {boolean} options.allowRotation allow rotation packing (default is false)
|
15 | * @property {boolean} options.tag allow auto grouping based on `rect.tag` (default is false)
|
16 | * @export
|
17 | * @interface Option
|
18 | */
|
19 | export interface IOption {
|
20 | smart?: boolean;
|
21 | pot?: boolean;
|
22 | square?: boolean;
|
23 | allowRotation?: boolean;
|
24 | tag?: boolean;
|
25 | }
|
26 |
|
27 | export class MaxRectsPacker<T extends IRectangle = Rectangle> {
|
28 |
|
29 | /**
|
30 | * The Bin array added to the packer
|
31 | *
|
32 | * @type {Bin[]}
|
33 | * @memberof MaxRectsPacker
|
34 | */
|
35 | public bins: Bin[];
|
36 |
|
37 | /**
|
38 | * Creates an instance of MaxRectsPacker.
|
39 | * @param {number} width of the output atlas (default is 4096)
|
40 | * @param {number} height of the output atlas (default is 4096)
|
41 | * @param {number} padding between glyphs/images (default is 0)
|
42 | * @param {IOption} [options={}] (Optional) packing options
|
43 | * @memberof MaxRectsPacker
|
44 | */
|
45 | constructor (
|
46 | public width: number = EDGE_MAX_VALUE,
|
47 | public height: number = EDGE_MAX_VALUE,
|
48 | public padding: number = 0,
|
49 | public options: IOption = { smart: true, pot: true, square: false, allowRotation: false, tag: false }
|
50 | ) {
|
51 | this.bins = [];
|
52 | }
|
53 |
|
54 | /**
|
55 | * Add a bin/rectangle object with data to packer
|
56 | * @param {number} width of the input bin/rectangle
|
57 | * @param {number} height of the input bin/rectangle
|
58 | * @param {*} data custom data object
|
59 | * @memberof MaxRectsPacker
|
60 | */
|
61 | public add (width: number, height: number, data: any): IRectangle;
|
62 | /**
|
63 | * Add a bin/rectangle object extends IRectangle to packer
|
64 | * @template T Generic type extends IRectangle interface
|
65 | * @param {T} rect the rect object add to the packer bin
|
66 | * @memberof MaxRectsPacker
|
67 | */
|
68 | public add (rect: T): T;
|
69 | public add (...args: any[]): any {
|
70 | let width: number;
|
71 | let height: number;
|
72 | let data: any;
|
73 | if (args.length === 1) {
|
74 | if (typeof args[0] !== 'object') throw new Error("MacrectsPacker.add(): Wrong parameters");
|
75 | const rect: T = args[0];
|
76 | if (rect.width > this.width || rect.height > this.height) {
|
77 | this.bins.push(new OversizedElementBin<T>(rect));
|
78 | } else {
|
79 | let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect) !== undefined);
|
80 | if (!added) {
|
81 | let bin = new MaxRectsBin<T>(this.width, this.height, this.padding, this.options);
|
82 | if (this.options.tag && rect.tag) bin.tag = rect.tag;
|
83 | bin.add(rect);
|
84 | this.bins.push(bin);
|
85 | }
|
86 | }
|
87 | } else {
|
88 | width = args[0];
|
89 | height = args[1];
|
90 | data = args.length > 2 ? args[2] : null;
|
91 | if (width > this.width || height > this.height) {
|
92 | this.bins.push(new OversizedElementBin<T>(width, height, data));
|
93 | } else {
|
94 | let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(width, height, data) !== undefined);
|
95 | if (!added) {
|
96 | let bin = new MaxRectsBin<T>(this.width, this.height, this.padding, this.options);
|
97 | if (this.options.tag && data.tag) bin.tag = data.tag;
|
98 | bin.add(width, height, data);
|
99 | this.bins.push(bin);
|
100 | }
|
101 | }
|
102 | }
|
103 | }
|
104 |
|
105 | /**
|
106 | * Add an Array of bins/rectangles to the packer.
|
107 | *
|
108 | * `Javascript`: Any object has property: { width, height, ... } is accepted.
|
109 | *
|
110 | * `Typescript`: object shall extends `MaxrectsPacker.IRectangle`.
|
111 | *
|
112 | * note: object has `hash` property will have more stable packing result
|
113 | *
|
114 | * @param {IRectangle[]} rects Array of bin/rectangles
|
115 | * @memberof MaxRectsPacker
|
116 | */
|
117 | public addArray (rects: T[]) {
|
118 | this.sort(rects).forEach(rect => this.add(rect));
|
119 | }
|
120 |
|
121 | /**
|
122 | * Stop adding new element to the current bin and return a new bin.
|
123 | *
|
124 | * note: After calling `next()` all elements will no longer added to previous bins.
|
125 | *
|
126 | * @returns {Bin}
|
127 | * @memberof MaxRectsPacker
|
128 | */
|
129 | public next (): number {
|
130 | this._currentBinIndex = this.bins.length;
|
131 | return this._currentBinIndex;
|
132 | }
|
133 |
|
134 | /**
|
135 | * Load bins to the packer, overwrite exist bins
|
136 | * @param {MaxRectsBin[]} bins MaxRectsBin objects
|
137 | * @memberof MaxRectsPacker
|
138 | */
|
139 | public load (bins: Bin[]) {
|
140 | bins.forEach((bin, index) => {
|
141 | if (bin.maxWidth > this.width || bin.maxHeight > this.height) {
|
142 | this.bins.push(new OversizedElementBin(bin.width, bin.height, {}));
|
143 | } else {
|
144 | let newBin = new MaxRectsBin<T>(this.width, this.height, this.padding, bin.options);
|
145 | newBin.freeRects.splice(0);
|
146 | bin.freeRects.forEach((r, i) => {
|
147 | newBin.freeRects.push(new Rectangle(r.width, r.height, r.x, r.y));
|
148 | });
|
149 | newBin.width = bin.width;
|
150 | newBin.height = bin.height;
|
151 | this.bins[index] = newBin;
|
152 | }
|
153 | }, this);
|
154 | }
|
155 |
|
156 | /**
|
157 | * Output current bins to save
|
158 | * @memberof MaxRectsPacker
|
159 | */
|
160 | public save (): IBin[] {
|
161 | let saveBins: IBin[] = [];
|
162 | this.bins.forEach((bin => {
|
163 | let saveBin: IBin = {
|
164 | width: bin.width,
|
165 | height: bin.height,
|
166 | maxWidth: bin.maxWidth,
|
167 | maxHeight: bin.maxHeight,
|
168 | freeRects: [],
|
169 | rects: [],
|
170 | options: bin.options
|
171 | };
|
172 | bin.freeRects.forEach(r => {
|
173 | saveBin.freeRects.push({
|
174 | x: r.x,
|
175 | y: r.y,
|
176 | width: r.width,
|
177 | height: r.height
|
178 | });
|
179 | });
|
180 | saveBins.push(saveBin);
|
181 | }));
|
182 | return saveBins;
|
183 | }
|
184 |
|
185 | /**
|
186 | * Sort the given rects based on longest edge
|
187 | *
|
188 | * If having same long edge, will sort second key `hash` if presented.
|
189 | *
|
190 | * @private
|
191 | * @param {T[]} rects
|
192 | * @returns
|
193 | * @memberof MaxRectsPacker
|
194 | */
|
195 | private sort (rects: T[]) {
|
196 | return rects.slice().sort((a, b) => {
|
197 | const result = Math.max(b.width, b.height) - Math.max(a.width, a.height);
|
198 | if (result === 0 && a.hash && b.hash) {
|
199 | return a.hash > b.hash ? -1 : 1;
|
200 | } else return result;
|
201 | });
|
202 | }
|
203 |
|
204 | private _currentBinIndex: number = 0;
|
205 | get currentBinIndex (): number { return this._currentBinIndex; }
|
206 | }
|