UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

245 lines (191 loc) 6.98 kB
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; } }