Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

51 expanding one dimension #54

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Creates a new Packer. maxWidth and maxHeight are passed on to all bins. If ```pa
- `options.tag` allow tag based group packing. (default is `false`)
- `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`)
- `options.border` atlas edge spacing (default is 0)
- `options.logic` how to fill the rects. There are three options: 0 (max area), 1 (max edge), 2 (fillWidth). Default is 1 (max edge)

#### ```packer.add(width, height, data)``` +1 overload

Expand Down Expand Up @@ -136,6 +137,18 @@ If `options.tag` is set to `true`, packer will check if the input object has `ta

Normally all bins are of equal size or smaller than ```maxWidth```/```maxHeight```. If a rect is added that individually does not fit into those constraints a special bin will be created. This bin will only contain a single rect with a special "oversized" flag. This can be handled further on in the chain by displaying an error/warning or by simply ignoring it.

## Packing logic

`options.logic` allows to change the method on how the algorithm selects the free spaces. There are three options:

`{option.logic = 0}` Logic is MAX_AREA, selects the free space with the smallest loss of area.

`{option.logic = 1}` Logic is MAX_EDGE, is default and selects the free space with the smallest loss of either width or height.

`{option.logic = 2}` Logic is FILL_WIDTH, fills the complete width first before placing elements in next row. To get the used height `bin.height` only gives correct values with options: `{pot: false, square: false}`. Best results also with `option.allowRotation = true`



## Packing algorithm

Use Max Rectangle Algorithm for packing, same as famous **Texture Packer**
49 changes: 42 additions & 7 deletions src/maxrects-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
i++;
}
this.pruneFreeList();
this.verticalExpand = this.width > this.height ? true : false;
this.verticalExpand = this.options.logic === PACKING_LOGIC.FILL_WIDTH ? false : this.width > this.height ? true : false;
rect.x = node.x;
rect.y = node.y;
if (rect.rot === undefined) rect.rot = false;
Expand Down Expand Up @@ -182,17 +182,42 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {
return undefined;
}

/**
* Find the best rect out of freeRects
* This method has different logics to resolve the best rect.
* @param width
* @param height
* @param allowRotation
* @returns Rectangle of the best rect for placement
*/
private findNode (width: number, height: number, allowRotation?: boolean): Rectangle | undefined {
// scoring based on one single number. The smaller the better the choice.
let score: number = Number.MAX_VALUE;
let areaFit: number;
let r: Rectangle;
let bestNode: Rectangle | undefined;
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 (this.options.logic === PACKING_LOGIC.MAX_AREA) {
// the rect with the lowest rest area wins
areaFit = r.width * r.height - width * height;
} else if (this.options.logic === PACKING_LOGIC.FILL_WIDTH) {
// this logic needs to factors to build a score.
// 1. rect position: choose the most rightest one with the lowest y coordinate.
// 2. size that needs to grow to place the element. The lower the better score (leads to optimal rotation placement)

const currentRectPositionScore = r.x + r.y * this.maxWidth; // each y value adds a full width to the score to balance x and y coordinates to a single number
const numberOfBetterRects = this.freeRects.filter(rect => (rect.x + rect.y * this.maxWidth) < currentRectPositionScore).length; // search if there are rects, righter and a lower y coordinate.

// calculate how much space will be add to total height
const heightToGain = r.y + height - this.height;

areaFit = numberOfBetterRects + heightToGain; // add both factors together
} else {
// the rect with the lowest loss of either width or height wins
areaFit = Math.min(r.width - width, r.height - height);
}
if (areaFit < score) {
bestNode = new Rectangle(width, height, r.x, r.y);
score = areaFit;
Expand All @@ -203,9 +228,19 @@ export class MaxRectsBin<T extends IRectangle = Rectangle> extends Bin<T> {

// 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 (this.options.logic === PACKING_LOGIC.MAX_AREA) {
areaFit = r.width * r.height - height * width;
} else if (this.options.logic === PACKING_LOGIC.FILL_WIDTH) {
const currentRectPositionScore = r.x + r.y * this.maxWidth;
const numberOfBetterRects = this.freeRects.filter(rect => (rect.x + rect.y * this.maxWidth) < currentRectPositionScore).length; // search if there are rects, righter and a lower y coordinate.

// calculate how much space will be add to total height
const heightToGain = r.y + width - this.height;

areaFit = numberOfBetterRects + heightToGain; // add both factors together
} else {
areaFit = 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;
Expand Down
7 changes: 4 additions & 3 deletions src/maxrects-packer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const EDGE_MAX_VALUE: number = 4096;
export const EDGE_MIN_VALUE: number = 128;
export enum PACKING_LOGIC {
MAX_AREA = 0,
MAX_EDGE = 1
MAX_EDGE = 1,
FILL_WIDTH = 2,
}

/**
Expand Down Expand Up @@ -110,7 +111,7 @@ export class MaxRectsPacker<T extends IRectangle = Rectangle> {
if (args.length === 1) {
if (typeof args[0] !== 'object') throw new Error("MacrectsPacker.add(): Wrong parameters");
const rect = args[0] as T;
if (rect.width > this.width || rect.height > this.height) {
if (!((rect.width <= this.width && rect.height <= this.height) || (this.options.allowRotation && rect.width <= this.height && rect.height <= this.width))) {
this.bins.push(new OversizedElementBin<T>(rect));
} else {
let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect) !== undefined);
Expand All @@ -127,7 +128,7 @@ export class MaxRectsPacker<T extends IRectangle = Rectangle> {
const rect: IRectangle = new Rectangle(args[0], args[1]);
if (args.length > 2) rect.data = args[2];

if (rect.width > this.width || rect.height > this.height) {
if (!((rect.width <= this.width && rect.height <= this.height) || (this.options.allowRotation && rect.width <= this.height && rect.height <= this.width))) {
this.bins.push(new OversizedElementBin<T>(rect as T));
} else {
let added = this.bins.slice(this._currentBinIndex).find(bin => bin.add(rect as T) !== undefined);
Expand Down
69 changes: 69 additions & 0 deletions test/maxrects-bin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,72 @@ describe("border", () => {
}
});
});

describe("logic FILL_WIDTH", () => {
beforeEach(() => {
bin = new MaxRectsBin(1024, 512, 0, {allowRotation: true, logic: 2, pot: false, square: false});
});

test("sets all elements along width with the smallest height", () => {
/**
* Visualize the placement result
* _______________________
* | ███ ███ ███ |
* ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
*/

let position1 = bin.add(300, 50, {});
let position2 = bin.add(50, 300, {});
let position3 = bin.add(300, 50, {});
expect(position1.x).toBe(0);
expect(position1.y).toBe(0);
expect(position2.x).toBe(300);
expect(position2.y).toBe(0);
expect(position3.x).toBe(600);
expect(position3.y).toBe(0);
expect(bin.width).toBe(900);
expect(bin.height).toBe(50);
});

test("adds rects correctly with rotation", () => {
/**
* Visualize the placement result (1 vertical at the end)
* _______________________
* | ███ ███ ███ █ |
* | ███ ███ ███ █ |
* | ███ ███ ███ █ |
* | ██████ |
* ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
*/

const rects = [
[300, 100],
[100, 300],
[300, 100],
[300, 100],
[100, 300],
[300, 100],
[300, 100],
[100, 300],
[300, 100],
[300, 100],
[300, 100],
[100, 600],
]
rects.forEach(rect => bin.add(rect[0], rect[1]));
expect([bin.rects[0].x, bin.rects[0].y]).toEqual([0, 0]);
expect([bin.rects[1].x, bin.rects[1].y]).toEqual([300, 0]);
expect([bin.rects[2].x, bin.rects[2].y]).toEqual([600, 0]);
expect([bin.rects[3].x, bin.rects[3].y]).toEqual([0, 100]);
expect([bin.rects[4].x, bin.rects[4].y]).toEqual([300, 100]);
expect([bin.rects[5].x, bin.rects[5].y]).toEqual([600, 100]);
expect([bin.rects[6].x, bin.rects[6].y]).toEqual([0, 200]);
expect([bin.rects[7].x, bin.rects[7].y]).toEqual([300, 200]);
expect([bin.rects[8].x, bin.rects[8].y]).toEqual([600, 200]);
expect([bin.rects[9].x, bin.rects[9].y]).toEqual([900, 0]);
expect([bin.rects[10].x, bin.rects[10].y]).toEqual([0, 300]);
expect(bin.width).toBe(1000);
expect(bin.height).toBe(400);
});

});
17 changes: 17 additions & 0 deletions test/maxrects-packer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ describe("#add", () => {
expect(packer.bins[1].rects[0].width).toBe(2000);
expect(packer.bins[1].rects[0].oversized).toBe(true);
});

test("checks oversized elements rotation and adds rotated", () => {
const packer = new MaxRectsPacker(512, 1024, 0, {...opt, allowRotation: true});
packer.add(640, 256, {num: 1});
expect(packer.bins.length).toBe(1);
expect(packer.bins[0].rects[0].width).toBe(256);
expect(packer.bins[0].rects[0].height).toBe(640);
expect(packer.bins[0].rects[0].oversized).toBe(false);
});

test("checks oversized elements and skip rotation when set to false", () => {
const packer = new MaxRectsPacker(512, 1024, 0, {...opt, allowRotation: false});
packer.add(640, 256, {num: 1});
expect(packer.bins.length).toBe(1);
expect(packer.bins[0].rects[0].width).toBe(640);
expect(packer.bins[0].rects[0].oversized).toBe(true);
});
});

describe("#sort", () => {
Expand Down