Skip to content

Commit

Permalink
feat: Per rectangle allowRotation
Browse files Browse the repository at this point in the history
Add rectangle.allowRotation parameter to override packer settings
  • Loading branch information
soimy committed Feb 13, 2020
1 parent 1d1b80e commit ab68e1c
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 19 deletions.
55 changes: 43 additions & 12 deletions src/geom/Rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,47 @@ export class Rectangle implements IRectangle {
* @param {number} [x=0]
* @param {number} [y=0]
* @param {boolean} [rot=false]
* @param {boolean} [allowRotation=false]
* @memberof Rectangle
*/
constructor (
width: number = 0,
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;
this._x = x;
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
Expand All @@ -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 &&
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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 ++;
}

Expand Down
22 changes: 17 additions & 5 deletions src/maxrects-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,16 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
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;
Expand All @@ -126,6 +135,7 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
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;
Expand Down Expand Up @@ -153,7 +163,7 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
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;
Expand All @@ -169,7 +179,9 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
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) ?
Expand All @@ -184,7 +196,7 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
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;

Expand Down Expand Up @@ -256,7 +268,7 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
}
}

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);
Expand Down
7 changes: 6 additions & 1 deletion test/efficiency.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}));
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 32 additions & 1 deletion test/maxrects-packer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"});
Expand All @@ -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)
})
});
16 changes: 16 additions & 0 deletions test/rectangle.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit ab68e1c

Please sign in to comment.