import { EDGE_MAX_VALUE, PACKING_LOGIC } from "./maxrects-packer"; import { Rectangle } from "./geom/Rectangle"; import { Bin } from "./abstract-bin"; export 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(); } } //# sourceMappingURL=maxrects-bin.js.map