UNPKG

7.26 kBPlain TextView Raw
1import { Rectangle, IRectangle } from "./geom/Rectangle";
2import { MaxRectsBin } from "./maxrects-bin";
3import { OversizedElementBin } from "./oversized-element-bin";
4import { Bin, IBin } from "./abstract-bin";
5
6export const EDGE_MAX_VALUE: number = 4096;
7export 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 */
19export interface IOption {
20 smart?: boolean;
21 pot?: boolean;
22 square?: boolean;
23 allowRotation?: boolean;
24 tag?: boolean;
25}
26
27export 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}