diff --git a/src/geom/Rectangle.ts b/src/geom/Rectangle.ts index 96588c9..436b920 100644 --- a/src/geom/Rectangle.ts +++ b/src/geom/Rectangle.ts @@ -23,6 +23,7 @@ export class Rectangle implements IRectangle { * @param {number} [x=0] * @param {number} [y=0] * @param {boolean} [rot=false] + * @param {boolean} [allowRotation=false] * @memberof Rectangle */ constructor ( @@ -30,7 +31,8 @@ export class Rectangle implements IRectangle { height: number = 0, x: number = 0, y: number = 0, - rot: boolean = false + rot: boolean = false, + allowRotation: boolean | undefined = undefined ) { this._width = width; this._height = height; @@ -38,29 +40,30 @@ export class Rectangle implements IRectangle { this._y = y; this._data = {}; this._rot = rot; + this._allowRotation = allowRotation; } /** * Test if two given rectangle collide each other * * @static - * @param {Rectangle} first - * @param {Rectangle} second + * @param {IRectangle} first + * @param {IRectangle} second * @returns * @memberof Rectangle */ - public static Collide (first: Rectangle, second: Rectangle) { return first.collide(second); } + public static Collide (first: IRectangle, second: IRectangle) { return first.collide(second); } /** * Test if the first rectangle contains the second one * * @static - * @param {Rectangle} first - * @param {Rectangle} second + * @param {IRectangle} first + * @param {IRectangle} second * @returns * @memberof Rectangle */ - public static Contain (first: Rectangle, second: Rectangle) { return first.contain(second); } + public static Contain (first: IRectangle, second: IRectangle) { return first.contain(second); } /** * Get the area (w * h) of the rectangle @@ -73,11 +76,11 @@ export class Rectangle implements IRectangle { /** * Test if the given rectangle collide with this rectangle. * - * @param {Rectangle} rect + * @param {IRectangle} rect * @returns {boolean} * @memberof Rectangle */ - public collide (rect: Rectangle): boolean { + public collide (rect: IRectangle): boolean { return ( rect.x < this.x + this.width && rect.x + rect.width > this.x && @@ -89,11 +92,11 @@ export class Rectangle implements IRectangle { /** * Test if this rectangle contains the given rectangle. * - * @param {Rectangle} rect + * @param {IRectangle} rect * @returns {boolean} * @memberof Rectangle */ - public contain (rect: Rectangle): boolean { + public contain (rect: IRectangle): boolean { 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); } @@ -148,6 +151,8 @@ export class Rectangle implements IRectangle { * @memberof Rectangle */ set rot (value: boolean) { + if (this._allowRotation === false) return; + if (this._rot !== value) { const tmp = this.width; this.width = this.height; @@ -157,11 +162,37 @@ export class Rectangle implements IRectangle { } } + protected _allowRotation: boolean | undefined = undefined; + + /** + * If the rectangle allow rotation + * + * @type {boolean} + * @memberof Rectangle + */ + get allowRotation (): boolean | undefined { return this._allowRotation; } + + /** + * Set the allowRotation tag of the rectangle. + * + * @memberof Rectangle + */ + set allowRotation (value: boolean | undefined) { + if (this._allowRotation !== value) { + this._allowRotation = value; + this._dirty ++; + } + } + protected _data: any; get data (): any { return this._data; } set data (value: any) { - if (value === this._data) return; + 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 ++; } diff --git a/src/maxrects-bin.ts b/src/maxrects-bin.ts index ae002a7..a2fb888 100644 --- a/src/maxrects-bin.ts +++ b/src/maxrects-bin.ts @@ -109,7 +109,16 @@ export class MaxRectsBin extends Bin { let tag = (rect.data && rect.data.tag) ? rect.data.tag : rect.tag ? rect.tag : undefined; if (this.options.tag && this.tag !== tag) return undefined; - let node: Rectangle | undefined = this.findNode(rect.width + this.padding, rect.height + this.padding); + let node: IRectangle | undefined; + let allowRotation: boolean | undefined; + // 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; @@ -126,6 +135,7 @@ export class MaxRectsBin extends Bin { 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 as T; @@ -153,7 +163,7 @@ export class MaxRectsBin extends Bin { return undefined; } - private findNode (width: number, height: number): Rectangle | undefined { + private findNode (width: number, height: number, allowRotation?: boolean): Rectangle | undefined { let score: number = Number.MAX_VALUE; let areaFit: number; let r: Rectangle; @@ -169,7 +179,9 @@ export class MaxRectsBin extends Bin { score = areaFit; } } - if (!this.options.allowRotation) continue; + + 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) ? @@ -184,7 +196,7 @@ export class MaxRectsBin extends Bin { return bestNode; } - private splitNode (freeRect: Rectangle, usedNode: Rectangle): boolean { + private splitNode (freeRect: IRectangle, usedNode: IRectangle): boolean { // Test if usedNode intersect with freeRect if (!freeRect.collide(usedNode)) return false; @@ -256,7 +268,7 @@ export class MaxRectsBin extends Bin { } } - private updateBinSize (node: Rectangle): boolean { + private updateBinSize (node: IRectangle): boolean { if (!this.options.smart) return false; if (this.stage.contain(node)) return false; let tmpWidth: number = Math.max(this.width, node.x + node.width - this.padding + this.border); diff --git a/test/efficiency.spec.js b/test/efficiency.spec.js index afef32d..12b6eb2 100644 --- a/test/efficiency.spec.js +++ b/test/efficiency.spec.js @@ -56,14 +56,19 @@ describe('Efficiency', () => { let results = results1.map((scenario, scenarioIndex) => scenario.map((result1, resultIndex) => { const result2 = results2[scenarioIndex][resultIndex]; if (result1.bins < result2.bins) { + result1.method = "E"; return result1; } else if (result1.bins > result2.bins) { + result2.method = "A"; return result2; } else if (result1.efficieny > result2.efficieny) { + result1.method = "E"; return result1; } else if (result1.efficieny < result2.efficieny) { + result2.method = "A"; return result2; } else { + result1.method = ""; return result1; } })); @@ -94,7 +99,7 @@ function createRows (results) { return SCENARIOS.map((scenario, i) => { return [i, rectSizeSum[i]].concat(results.map(resultCandidate => { let result = resultCandidate[i]; - return `${toPercent(result.efficieny)} (${result.bins} bins)`; + return `${toPercent(result.efficieny)} (${result.bins} bins)${result.method}`; })); }).concat([["sum", ""].concat(results.map(result => { let usedSize = result.reduce((memo, data) => memo + data.usedSize, 0); diff --git a/test/maxrects-packer.spec.js b/test/maxrects-packer.spec.js index a315e5b..cc172d8 100644 --- a/test/maxrects-packer.spec.js +++ b/test/maxrects-packer.spec.js @@ -214,7 +214,7 @@ describe("misc functionalities", () => { }); test("quick repack & deep repack", () => { - packer = new MaxRectsPacker(1024, 1024, 0, {...opt, ...{tag: true}}); + packer = new MaxRectsPacker(1024, 1024, 0, {...opt, ...{ tag: true }}); let rect = packer.add(1024, 512, {hash: "6"}); packer.add(512, 512, {hash: "5"}); packer.add(512, 512, {hash: "4"}); @@ -236,4 +236,35 @@ describe("misc functionalities", () => { packer.repack(false); // deep repack expect(packer.bins.length).toBe(2); }); + + test("Packer allow rotation", () => { + packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: true }}); + packer.add(398, 98); + packer.add(398, 98); + packer.add(398, 98); + let x = packer.add(398, 98); + expect(x.rot).toBe(true); + }); + + test("Per rectangle allow rotation", () => { + packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: true }}); + packer.add(448, 98); + packer.add(448, 98); + packer.add(448, 98); + packer.add(448, 98); + // false overriding + let x = packer.add(398, 48, { allowRotation: false }); + expect(packer.bins.length).toBe(2); + expect(x.rot).toBe(false); + + packer = new MaxRectsPacker(500, 400, 1, {...opt, ...{ smart: false, allowRotation: false}}); + packer.add(448, 98); + packer.add(448, 98); + packer.add(448, 98); + packer.add(448, 98); + // true overriding + x = packer.add(398, 48, { allowRotation: true }); + expect(packer.bins.length).toBe(1); + expect(x.rot).toBe(true) + }) }); diff --git a/test/rectangle.spec.js b/test/rectangle.spec.js index 5fca72d..dd11108 100644 --- a/test/rectangle.spec.js +++ b/test/rectangle.spec.js @@ -67,6 +67,22 @@ describe("Rectangle", () => { expect(rect.dirty).toBe(true); }); + test("allowRotation setting", () => { + const rect = new Rectangle(512, 256, 0, 0, false, true); + expect(rect.allowRotation).toBe(true); + rect.allowRotation = false; + expect(rect.allowRotation).toBe(false); + }); + + test("data.allowRotation sync", () => { + const rect = new Rectangle(512, 256); + expect(rect.allowRotation).toBeUndefined(); + rect.data = { allowRotation: false }; + expect(rect.allowRotation).toBe(false); + rect.data = { allowRotation: true }; + expect(rect.allowRotation).toBe(true); + }); + test("method: area()", () => { const rect = new Rectangle(16, 16); expect(rect.area()).toBe(256);