class Rectangle { /** * Creates an instance of Rectangle. * * @param {number} [width=0] * @param {number} [height=0] * @param {number} [x=0] * @param {number} [y=0] * @param {boolean} [rot=false] * @param {boolean} [allowRotation=false] * @memberof Rectangle */ constructor(width = 0, height = 0, x = 0, y = 0, rot = false, allowRotation = undefined) { /** * Oversized tag on rectangle which is bigger than packer itself. * * @type {boolean} * @memberof Rectangle */ this.oversized = false; this._rot = false; this._allowRotation = undefined; this._dirty = 0; this._width = width; this._height = height; this._x = x; this._y = y; this._data = {}; this._rot = rot; this._allowRotation = allowRotation; } /** * Test if two given rectangle collide each other * * @static * @param {IRectangle} first * @param {IRectangle} second * @returns * @memberof Rectangle */ static Collide(first, second) { return first.collide(second); } /** * Test if the first rectangle contains the second one * * @static * @param {IRectangle} first * @param {IRectangle} second * @returns * @memberof Rectangle */ static Contain(first, second) { return first.contain(second); } /** * Get the area (w * h) of the rectangle * * @returns {number} * @memberof Rectangle */ area() { return this.width * this.height; } /** * Test if the given rectangle collide with this rectangle. * * @param {IRectangle} rect * @returns {boolean} * @memberof Rectangle */ collide(rect) { return (rect.x < this.x + this.width && rect.x + rect.width > this.x && rect.y < this.y + this.height && rect.y + rect.height > this.y); } /** * Test if this rectangle contains the given rectangle. * * @param {IRectangle} rect * @returns {boolean} * @memberof Rectangle */ contain(rect) { return (rect.x >= this.x && rect.y >= this.y && rect.x + rect.width <= this.x + this.width && rect.y + rect.height <= this.y + this.height); } get width() { return this._width; } set width(value) { if (value === this._width) return; this._width = value; this._dirty++; } get height() { return this._height; } set height(value) { if (value === this._height) return; this._height = value; this._dirty++; } get x() { return this._x; } set x(value) { if (value === this._x) return; this._x = value; this._dirty++; } get y() { return this._y; } set y(value) { if (value === this._y) return; this._y = value; this._dirty++; } /** * If the rectangle is rotated * * @type {boolean} * @memberof Rectangle */ get rot() { return this._rot; } /** * Set the rotate tag of the rectangle. * * note: after `rot` is set, `width/height` of this rectangle is swaped. * * @memberof Rectangle */ set rot(value) { if (this._allowRotation === false) return; if (this._rot !== value) { const tmp = this.width; this.width = this.height; this.height = tmp; this._rot = value; this._dirty++; } } /** * If the rectangle allow rotation * * @type {boolean} * @memberof Rectangle */ get allowRotation() { return this._allowRotation; } /** * Set the allowRotation tag of the rectangle. * * @memberof Rectangle */ set allowRotation(value) { if (this._allowRotation !== value) { this._allowRotation = value; this._dirty++; } } get data() { return this._data; } set data(value) { if (value === null || value === this._data) return; this._data = value; // extract allowRotation settings if (typeof value === "object" && value.hasOwnProperty("allowRotation")) { this._allowRotation = value.allowRotation; } this._dirty++; } get dirty() { return this._dirty > 0; } setDirty(value = true) { this._dirty = value ? this._dirty + 1 : 0; } } class Bin { constructor() { this._dirty = 0; } get dirty() { return this._dirty > 0 || this.rects.some(rect => rect.dirty); } /** * Set bin dirty status * * @memberof Bin */ setDirty(value = true) { this._dirty = value ? this._dirty + 1 : 0; if (!value) { for (let rect of this.rects) { if (rect.setDirty) rect.setDirty(false); } } } } class MaxRectsBin extends Bin { constructor(maxWidth = EDGE_MAX_VALUE, maxHeight = EDGE_MAX_VALUE, padding = 0, options = {}) { super(); this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.padding = padding; this.freeRects = []; this.rects = []; this.verticalExpand = false; this.options = { smart: true, pot: true, square: true, allowRotation: false, tag: false, exclusiveTag: true, border: 0, logic: PACKING_LOGIC.MAX_EDGE }; this.options = Object.assign(Object.assign({}, this.options), options); this.width = this.options.smart ? 0 : maxWidth; this.height = this.options.smart ? 0 : maxHeight; this.border = this.options.border ? this.options.border : 0; this.freeRects.push(new Rectangle(this.maxWidth + this.padding - this.border * 2, this.maxHeight + this.padding - this.border * 2, this.border, this.border)); this.stage = new Rectangle(this.width, this.height); } add(...args) { let data; let rect; if (args.length === 1) { if (typeof args[0] !== 'object') throw new Error("MacrectsBin.add(): Wrong parameters"); rect = args[0]; // Check if rect.tag match bin.tag, if bin.tag not defined, it will accept any rect let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; if (this.options.tag && this.options.exclusiveTag && this.tag !== tag) return undefined; } else { data = args.length > 2 ? args[2] : null; // Check if data.tag match bin.tag, if bin.tag not defined, it will accept any rect if (this.options.tag && this.options.exclusiveTag) { if (data && this.tag !== data.tag) return undefined; if (!data && this.tag) return undefined; } rect = new Rectangle(args[0], args[1]); rect.data = data; rect.setDirty(false); } const result = this.place(rect); if (result) this.rects.push(result); return result; } repack() { let unpacked = []; this.reset(); // re-sort rects from big to small this.rects.sort((a, b) => { const result = Math.max(b.width, b.height) - Math.max(a.width, a.height); if (result === 0 && a.hash && b.hash) { return a.hash > b.hash ? -1 : 1; } else return result; }); for (let rect of this.rects) { if (!this.place(rect)) { unpacked.push(rect); } } for (let rect of unpacked) this.rects.splice(this.rects.indexOf(rect), 1); return unpacked.length > 0 ? unpacked : undefined; } reset(deepReset = false, resetOption = false) { if (deepReset) { if (this.data) delete this.data; if (this.tag) delete this.tag; this.rects = []; if (resetOption) { this.options = { smart: true, pot: true, square: true, allowRotation: false, tag: false, border: 0 }; } } this.width = this.options.smart ? 0 : this.maxWidth; this.height = this.options.smart ? 0 : this.maxHeight; this.border = this.options.border ? this.options.border : 0; this.freeRects = [new Rectangle(this.maxWidth + this.padding - this.border * 2, this.maxHeight + this.padding - this.border * 2, this.border, this.border)]; this.stage = new Rectangle(this.width, this.height); this._dirty = 0; } clone() { let clonedBin = new MaxRectsBin(this.maxWidth, this.maxHeight, this.padding, this.options); for (let rect of this.rects) { clonedBin.add(rect); } return clonedBin; } place(rect) { // recheck if tag matched let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; if (this.options.tag && this.options.exclusiveTag && this.tag !== tag) return undefined; let node; let allowRotation; // getter/setter do not support hasOwnProperty() if (rect.hasOwnProperty("_allowRotation") && rect.allowRotation !== undefined) { allowRotation = rect.allowRotation; // Per Rectangle allowRotation override packer settings } else { allowRotation = this.options.allowRotation; } node = this.findNode(rect.width + this.padding, rect.height + this.padding, allowRotation); if (node) { this.updateBinSize(node); let numRectToProcess = this.freeRects.length; let i = 0; while (i < numRectToProcess) { if (this.splitNode(this.freeRects[i], node)) { this.freeRects.splice(i, 1); numRectToProcess--; i--; } i++; } this.pruneFreeList(); this.verticalExpand = this.width > this.height ? true : false; rect.x = node.x; rect.y = node.y; if (rect.rot === undefined) rect.rot = false; rect.rot = node.rot ? !rect.rot : rect.rot; this._dirty++; return rect; } else if (!this.verticalExpand) { if (this.updateBinSize(new Rectangle(rect.width + this.padding, rect.height + this.padding, this.width + this.padding - this.border, this.border)) || this.updateBinSize(new Rectangle(rect.width + this.padding, rect.height + this.padding, this.border, this.height + this.padding - this.border))) { return this.place(rect); } } else { if (this.updateBinSize(new Rectangle(rect.width + this.padding, rect.height + this.padding, this.border, this.height + this.padding - this.border)) || this.updateBinSize(new Rectangle(rect.width + this.padding, rect.height + this.padding, this.width + this.padding - this.border, this.border))) { return this.place(rect); } } return undefined; } findNode(width, height, allowRotation) { let score = Number.MAX_VALUE; let areaFit; let r; let bestNode; for (let i in this.freeRects) { r = this.freeRects[i]; if (r.width >= width && r.height >= height) { areaFit = (this.options.logic === PACKING_LOGIC.MAX_AREA) ? r.width * r.height - width * height : Math.min(r.width - width, r.height - height); if (areaFit < score) { bestNode = new Rectangle(width, height, r.x, r.y); score = areaFit; } } if (!allowRotation) continue; // Continue to test 90-degree rotated rectangle if (r.width >= height && r.height >= width) { areaFit = (this.options.logic === PACKING_LOGIC.MAX_AREA) ? r.width * r.height - height * width : Math.min(r.height - width, r.width - height); if (areaFit < score) { bestNode = new Rectangle(height, width, r.x, r.y, true); // Rotated node score = areaFit; } } } return bestNode; } splitNode(freeRect, usedNode) { // Test if usedNode intersect with freeRect if (!freeRect.collide(usedNode)) return false; // Do vertical split if (usedNode.x < freeRect.x + freeRect.width && usedNode.x + usedNode.width > freeRect.x) { // New node at the top side of the used node if (usedNode.y > freeRect.y && usedNode.y < freeRect.y + freeRect.height) { let newNode = new Rectangle(freeRect.width, usedNode.y - freeRect.y, freeRect.x, freeRect.y); this.freeRects.push(newNode); } // New node at the bottom side of the used node if (usedNode.y + usedNode.height < freeRect.y + freeRect.height) { let newNode = new Rectangle(freeRect.width, freeRect.y + freeRect.height - (usedNode.y + usedNode.height), freeRect.x, usedNode.y + usedNode.height); this.freeRects.push(newNode); } } // Do Horizontal split if (usedNode.y < freeRect.y + freeRect.height && usedNode.y + usedNode.height > freeRect.y) { // New node at the left side of the used node. if (usedNode.x > freeRect.x && usedNode.x < freeRect.x + freeRect.width) { let newNode = new Rectangle(usedNode.x - freeRect.x, freeRect.height, freeRect.x, freeRect.y); this.freeRects.push(newNode); } // New node at the right side of the used node. if (usedNode.x + usedNode.width < freeRect.x + freeRect.width) { let newNode = new Rectangle(freeRect.x + freeRect.width - (usedNode.x + usedNode.width), freeRect.height, usedNode.x + usedNode.width, freeRect.y); this.freeRects.push(newNode); } } return true; } pruneFreeList() { // Go through each pair of freeRects and remove any rects that is redundant let i = 0; let j = 0; let len = this.freeRects.length; while (i < len) { j = i + 1; let tmpRect1 = this.freeRects[i]; while (j < len) { let tmpRect2 = this.freeRects[j]; if (tmpRect2.contain(tmpRect1)) { this.freeRects.splice(i, 1); i--; len--; break; } if (tmpRect1.contain(tmpRect2)) { this.freeRects.splice(j, 1); j--; len--; } j++; } i++; } } updateBinSize(node) { if (!this.options.smart) return false; if (this.stage.contain(node)) return false; let tmpWidth = Math.max(this.width, node.x + node.width - this.padding + this.border); let tmpHeight = Math.max(this.height, node.y + node.height - this.padding + this.border); if (this.options.allowRotation) { // do extra test on rotated node whether it's a better choice const rotWidth = Math.max(this.width, node.x + node.height - this.padding + this.border); const rotHeight = Math.max(this.height, node.y + node.width - this.padding + this.border); if (rotWidth * rotHeight < tmpWidth * tmpHeight) { tmpWidth = rotWidth; tmpHeight = rotHeight; } } if (this.options.pot) { tmpWidth = Math.pow(2, Math.ceil(Math.log(tmpWidth) * Math.LOG2E)); tmpHeight = Math.pow(2, Math.ceil(Math.log(tmpHeight) * Math.LOG2E)); } if (this.options.square) { tmpWidth = tmpHeight = Math.max(tmpWidth, tmpHeight); } if (tmpWidth > this.maxWidth + this.padding || tmpHeight > this.maxHeight + this.padding) { return false; } this.expandFreeRects(tmpWidth + this.padding, tmpHeight + this.padding); this.width = this.stage.width = tmpWidth; this.height = this.stage.height = tmpHeight; return true; } expandFreeRects(width, height) { this.freeRects.forEach((freeRect, index) => { if (freeRect.x + freeRect.width >= Math.min(this.width + this.padding - this.border, width)) { freeRect.width = width - freeRect.x - this.border; } if (freeRect.y + freeRect.height >= Math.min(this.height + this.padding - this.border, height)) { freeRect.height = height - freeRect.y - this.border; } }, this); this.freeRects.push(new Rectangle(width - this.width - this.padding, height - this.border * 2, this.width + this.padding - this.border, this.border)); this.freeRects.push(new Rectangle(width - this.border * 2, height - this.height - this.padding, this.border, this.height + this.padding - this.border)); this.freeRects = this.freeRects.filter(freeRect => { return !(freeRect.width <= 0 || freeRect.height <= 0 || freeRect.x < this.border || freeRect.y < this.border); }); this.pruneFreeList(); } } class OversizedElementBin extends Bin { constructor(...args) { super(); this.rects = []; if (args.length === 1) { if (typeof args[0] !== 'object') throw new Error("OversizedElementBin: Wrong parameters"); const rect = args[0]; this.rects = [rect]; this.width = rect.width; this.height = rect.height; this.data = rect.data; rect.oversized = true; } else { this.width = args[0]; this.height = args[1]; this.data = args.length > 2 ? args[2] : null; const rect = new Rectangle(this.width, this.height); rect.oversized = true; rect.data = this.data; this.rects.push(rect); } this.freeRects = []; this.maxWidth = this.width; this.maxHeight = this.height; this.options = { smart: false, pot: false, square: false }; } add() { return undefined; } reset(deepReset = false) { // nothing to do here } repack() { return undefined; } clone() { let clonedBin = new OversizedElementBin(this.rects[0]); return clonedBin; } } const EDGE_MAX_VALUE = 4096; var PACKING_LOGIC; (function (PACKING_LOGIC) { PACKING_LOGIC[PACKING_LOGIC["MAX_AREA"] = 0] = "MAX_AREA"; PACKING_LOGIC[PACKING_LOGIC["MAX_EDGE"] = 1] = "MAX_EDGE"; })(PACKING_LOGIC || (PACKING_LOGIC = {})); class MaxRectsPacker { /** * Creates an instance of MaxRectsPacker. * @param {number} width of the output atlas (default is 4096) * @param {number} height of the output atlas (default is 4096) * @param {number} padding between glyphs/images (default is 0) * @param {IOption} [options={}] (Optional) packing options * @memberof MaxRectsPacker */ constructor(width = EDGE_MAX_VALUE, height = EDGE_MAX_VALUE, padding = 0, options = {}) { this.width = width; this.height = height; this.padding = padding; /** * Options for MaxRect Packer * @property {boolean} options.smart Smart sizing packer (default is true) * @property {boolean} options.pot use power of 2 sizing (default is true) * @property {boolean} options.square use square size (default is false) * @property {boolean} options.allowRotation allow rotation packing (default is false) * @property {boolean} options.tag allow auto grouping based on `rect.tag` (default is false) * @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) * @property {boolean} options.border atlas edge spacing (default is 0) * @property {PACKING_LOGIC} options.logic MAX_AREA or MAX_EDGE based sorting logic (default is MAX_EDGE) * @export * @interface Option */ this.options = { smart: true, pot: true, square: false, allowRotation: false, tag: false, exclusiveTag: true, border: 0, logic: PACKING_LOGIC.MAX_EDGE }; this._currentBinIndex = 0; this.bins = []; this.options = Object.assign(Object.assign({}, this.options), options); } add(...args) { if (args.length === 1) { if (typeof args[0] !== 'object') throw new Error("MacrectsPacker.add(): Wrong parameters"); const rect = args[0]; if (rect.width > this.width || rect.height > this.height) { this.bins.push(new OversizedElementBin(rect)); } else { let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect) !== undefined); if (!added) { let bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; if (this.options.tag && tag) bin.tag = tag; bin.add(rect); this.bins.push(bin); } } return rect; } else { const rect = new Rectangle(args[0], args[1]); if (args.length > 2) rect.data = args[2]; if (rect.width > this.width || rect.height > this.height) { this.bins.push(new OversizedElementBin(rect)); } else { let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect) !== undefined); if (!added) { let bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); if (this.options.tag && rect.data.tag) bin.tag = rect.data.tag; bin.add(rect); this.bins.push(bin); } } return rect; } } /** * Add an Array of bins/rectangles to the packer. * * `Javascript`: Any object has property: { width, height, ... } is accepted. * * `Typescript`: object shall extends `MaxrectsPacker.IRectangle`. * * note: object has `hash` property will have more stable packing result * * @param {IRectangle[]} rects Array of bin/rectangles * @memberof MaxRectsPacker */ addArray(rects) { if (!this.options.tag || this.options.exclusiveTag) { // if not using tag or using exclusiveTag, old approach this.sort(rects, this.options.logic).forEach(rect => this.add(rect)); } else { // sort rects by tags first if (rects.length === 0) return; rects.sort((a, b) => { const aTag = (a.data && a.data.tag) ? a.data.tag : a.tag ? a.tag : undefined; const bTag = (b.data && b.data.tag) ? b.data.tag : b.tag ? b.tag : undefined; return bTag === undefined ? -1 : aTag === undefined ? 1 : bTag > aTag ? -1 : 1; }); // iterate all bins to find the first bin which can place rects with same tag // let currentTag; let currentIdx = 0; let targetBin = this.bins.slice(this._currentBinIndex).find((bin, binIndex) => { let testBin = bin.clone(); for (let i = currentIdx; i < rects.length; i++) { const rect = rects[i]; const tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; // initialize currentTag if (i === 0) currentTag = tag; if (tag !== currentTag) { // all current tag memeber tested successfully currentTag = tag; // do addArray() this.sort(rects.slice(currentIdx, i), this.options.logic).forEach(r => bin.add(r)); currentIdx = i; // recrusively addArray() with remaining rects this.addArray(rects.slice(i)); return true; } // remaining untagged rect will use normal addArray() if (tag === undefined) { // do addArray() this.sort(rects.slice(i), this.options.logic).forEach(r => this.add(r)); currentIdx = rects.length; // end test return true; } // still in the same tag group if (testBin.add(rect) === undefined) { // add the rects that could fit into the bins already // do addArray() this.sort(rects.slice(currentIdx, i), this.options.logic).forEach(r => bin.add(r)); currentIdx = i; // current bin cannot contain all tag members // procceed to test next bin return false; } } // all rects tested // do addArray() to the remaining tag group this.sort(rects.slice(currentIdx), this.options.logic).forEach(r => bin.add(r)); return true; }); // create a new bin if no current bin fit if (!targetBin) { const rect = rects[currentIdx]; const bin = new MaxRectsBin(this.width, this.height, this.padding, this.options); const tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; if (this.options.tag && this.options.exclusiveTag && tag) bin.tag = tag; this.bins.push(bin); // Add the rect to the newly created bin bin.add(rect); currentIdx++; this.addArray(rects.slice(currentIdx)); } } } /** * Reset entire packer to initial states, keep settings * * @memberof MaxRectsPacker */ reset() { this.bins = []; this._currentBinIndex = 0; } /** * Repack all elements inside bins * * @param {boolean} [quick=true] quick repack only dirty bins * @returns {void} * @memberof MaxRectsPacker */ repack(quick = true) { if (quick) { let unpack = []; for (let bin of this.bins) { if (bin.dirty) { let up = bin.repack(); if (up) unpack.push(...up); } } this.addArray(unpack); return; } if (!this.dirty) return; const allRects = this.rects; this.reset(); this.addArray(allRects); } /** * Stop adding new element to the current bin and return a new bin. * * note: After calling `next()` all elements will no longer added to previous bins. * * @returns {Bin} * @memberof MaxRectsPacker */ next() { this._currentBinIndex = this.bins.length; return this._currentBinIndex; } /** * Load bins to the packer, overwrite exist bins * @param {MaxRectsBin[]} bins MaxRectsBin objects * @memberof MaxRectsPacker */ load(bins) { bins.forEach((bin, index) => { if (bin.maxWidth > this.width || bin.maxHeight > this.height) { this.bins.push(new OversizedElementBin(bin.width, bin.height, {})); } else { let newBin = new MaxRectsBin(this.width, this.height, this.padding, bin.options); newBin.freeRects.splice(0); bin.freeRects.forEach((r, i) => { newBin.freeRects.push(new Rectangle(r.width, r.height, r.x, r.y)); }); newBin.width = bin.width; newBin.height = bin.height; if (bin.tag) newBin.tag = bin.tag; this.bins[index] = newBin; } }, this); } /** * Output current bins to save * @memberof MaxRectsPacker */ save() { let saveBins = []; this.bins.forEach((bin => { let saveBin = { width: bin.width, height: bin.height, maxWidth: bin.maxWidth, maxHeight: bin.maxHeight, freeRects: [], rects: [], options: bin.options }; if (bin.tag) saveBin = Object.assign(Object.assign({}, saveBin), { tag: bin.tag }); bin.freeRects.forEach(r => { saveBin.freeRects.push({ x: r.x, y: r.y, width: r.width, height: r.height }); }); saveBins.push(saveBin); })); return saveBins; } /** * Sort the given rects based on longest edge or surface area. * * If rects have the same sort value, will sort by second key `hash` if presented. * * @private * @param {T[]} rects * @param {PACKING_LOGIC} [logic=PACKING_LOGIC.MAX_EDGE] sorting logic, "area" or "edge" * @returns * @memberof MaxRectsPacker */ sort(rects, logic = PACKING_LOGIC.MAX_EDGE) { return rects.slice().sort((a, b) => { const result = (logic === PACKING_LOGIC.MAX_EDGE) ? Math.max(b.width, b.height) - Math.max(a.width, a.height) : b.width * b.height - a.width * a.height; if (result === 0 && a.hash && b.hash) { return a.hash > b.hash ? -1 : 1; } else return result; }); } /** * Return current functioning bin index, perior to this wont accept any new elements * * @readonly * @type {number} * @memberof MaxRectsPacker */ get currentBinIndex() { return this._currentBinIndex; } /** * Returns dirty status of all child bins * * @readonly * @type {boolean} * @memberof MaxRectsPacker */ get dirty() { return this.bins.some(bin => bin.dirty); } /** * Return all rectangles in this packer * * @readonly * @type {T[]} * @memberof MaxRectsPacker */ get rects() { let allRects = []; for (let bin of this.bins) { allRects.push(...bin.rects); } return allRects; } } export { Bin, MaxRectsBin, MaxRectsPacker, OversizedElementBin, PACKING_LOGIC, Rectangle }; //# sourceMappingURL=maxrects-packer.mjs.map