@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
245 lines (191 loc) • 6.98 kB
JavaScript
import { assert } from "../../../assert.js";
import BinaryHeap from "../../../collection/heap/BinaryHeap.js";
import { QuadTreeDatum } from "../../2d/quad-tree/QuadTreeDatum.js";
import { QuadTreeNode } from "../../2d/quad-tree/QuadTreeNode.js";
import Vector2 from "../../Vector2.js";
import { costByRemainingArea } from "./cost/costByRemainingArea.js";
import { findBestContainer } from "./findBestContainer.js";
import { packOneBox } from "./packOneBox.js";
import { removeRedundantBoxes } from "./removeRedundantBoxes.js";
/**
* Packs rectangles into a finite area, packer is incremental and supports both insertions and removals of rectangles.
* Implementation of "max rectangles" packing algorithm.
* Useful for packing texture atlases.
*
* @see "A Thousand Ways to Pack the Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing" 2010 Jukka Jylänki
*/
export class MaxRectanglesPacker {
/**
*
* @param {number} width
* @param {number} height
*/
constructor(width, height) {
this.size = new Vector2(width, height);
/**
*
* @type {QuadTreeNode}
*/
this.free = new QuadTreeNode(0, 0, width, height);
// initialize a with a free space occupying the entire area
this.free.add(null, 0, 0, width, height);
/**
* Currently held boxes.
* Managed internally, do not modify.
* @readonly
* @type {AABB2[]}
*/
this.boxes = [];
}
/**
*
* @param {AABB2} box exact box to remove
* @returns {boolean} true when box is removed, false if not found
*/
remove(box) {
const i = this.boxes.indexOf(box);
if (i === -1) {
//not found
return false;
}
this.boxes.splice(i, 1);
// introduce a free node in the unoccupied space
this.free.insertDatum(new QuadTreeDatum(box.x0, box.y0, box.x1, box.y1));
// assert.ok(this.validate());
return true;
}
/**
*
* @param {AABB2[]} boxes
* @returns {number} How many failed to be removed. Each failure represents a box which was not packed in the first place.
*/
removeMany(boxes) {
let failures = 0;
const l = boxes.length;
for (let i = 0; i < l; i++) {
const box = boxes[i];
if (!this.remove(box)) {
failures++;
}
}
return failures;
}
/**
* Pack a given box.
* Atomic, if box fails packing - the existing data structure is unchanged.
* @param {AABB2} box
* @returns {boolean} true if box was successfully added into the packing, false otherwise
*/
add(box) {
const success = packOneBox(box, this.free);
if (success) {
this.boxes.push(box);
removeRedundantBoxes(this.free);
}
// assert.ok(this.validate());
return success;
}
/**
* Tests whether a rectangle of given dimensions can be packed into remaining space
* Essentially, if this method succeeds - insertion will succeed as well, and if it fails - insertion will fail too
* @param {number} w
* @param {number} h
* @return {boolean}
*/
canAdd(w, h) {
return findBestContainer(w, h, this.free, costByRemainingArea) !== null;
}
/**
* Method is transactional, if one box fails to be packed, all fail and packer is reverted to original state
* @param {AABB2[]} boxes
* @returns {boolean}
*/
addMany(boxes) {
assert.defined(boxes, 'boxes');
assert.isArray(boxes, 'boxes');
// assert.ok(this.validate());
const numBoxes = boxes.length;
const packed = [];
/**
*
* @param {number} boxIndex
* @returns {number}
*/
function scoreBoxByMinSide(boxIndex) {
const box = boxes[boxIndex];
return -Math.min(box.getWidth(), box.getHeight());
}
const heap = new BinaryHeap(scoreBoxByMinSide);
for (let i = 0; i < numBoxes; i++) {
heap.push(i);
}
for (let i = 0; i < numBoxes; i++) {
const boxIndex = heap.pop();
const box = boxes[boxIndex];
if (!packOneBox(box, this.free)) {
//remove whatever has been packed
const removeFailCount = this.removeMany(packed);
assert.equal(removeFailCount, 0, `Failed to remove ${removeFailCount} boxes`);
return false;
} else {
this.boxes.push(box);
//box packed successfully
packed.push(box);
}
}
removeRedundantBoxes(this.free);
return true;
}
/**
* Re-packs all rectangles. If repack fails - all boxes are removed.
*
* @returns {boolean} true if successful, false if there was not enough space found during packing
*/
repack() {
// remember existing boxes
const boxes = this.boxes;
// reset packer
this.clear();
// pack again
return this.addMany(boxes);
}
/**
* Clear out all the data from the packer
*/
clear() {
this.free.clear();
this.free.insertDatum(new QuadTreeDatum(0, 0, this.size.x, this.size.y));
this.boxes = [];
}
/**
* Resize the packer canvas, may trigger repacking if new dimensions are smaller than the existing ones
* @param {number} width
* @param {number} height
* @returns {boolean} false if packing fails after resize, true otherwise
*/
resize(width, height) {
assert.isNumber(width, 'width');
assert.isNumber(height, 'height');
assert.isFinite(width, 'width');
assert.isFinite(height, 'height');
assert.greaterThan(width, 0, 'width');
assert.greaterThan(height, 0, 'height');
// assert.ok(this.validate());
const oldWidth = this.size.x;
const oldHeight = this.size.y;
this.size.set(width, height);
if (oldWidth > width || oldHeight > height) {
// canvas was made smaller in at least one dimension, re-pack is required
return this.repack();
}
//canvas was enlarged, we can simply add new free areas
if (width > oldWidth) {
this.free.insertDatum(new QuadTreeDatum(oldWidth, 0, width, height));
}
if (height > oldHeight) {
this.free.insertDatum(new QuadTreeDatum(0, oldHeight, width, height))
}
// assert.ok(this.validate());
return true;
}
}