All files / engine/Source/Core TexturePacker.js

100% Statements 45/45
100% Branches 26/26
100% Functions 4/4
100% Lines 45/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182                                                      2422x         2422x       2422x           2422x                           671x 671x   671x   671x                             1x   649x 649x 649x     649x 649x 163x     486x 486x           1x 2984x 38x       2946x 2186x   404x     1782x   1782x 1782x 1782x 1782x     1782x 393x       1389x 486x     903x     903x 450x               450x   450x 428x               450x       453x               453x   453x 420x               453x       760x              
import BoundingRectangle from "./BoundingRectangle.js";
import Check from "./Check.js";
import defined from "./defined.js";
 
/**
 * @typedef {object} TexturePacker.PackableObject
 * Any object, such as an <code>Image</code> with the following properties:
 * @private
 * @property {number} width The width of the image, or other object, usually in pixels
 * @property {number} height The height of the image, or other object, usually in pixels
 */
 
/**
 * A texture atlas is recursively broken down into regions of space called nodes.
 * Nodes contain either an image reference or child nodes.
 * @private
 * @constructor
 * @param {object} options An options object with the following properties:
 * @param {number} options.x The x-offset of the texture node
 * @param {number} options.y The y-offset of the texture node
 * @param {number} options.width The width of the texture node
 * @param {number} options.height The width of the texture node
 */
function TextureNode({ x, y, width, height }) {
  /**
   * @type {BoundingRectangle}
   */
  this.rectangle = new BoundingRectangle(x, y, width, height);
 
  /**
   * @type {TextureNode|undefined}
   */
  this.childNode1 = undefined;
  /**
   * @type {TextureNode|undefined}
   */
  this.childNode2 = undefined;
 
  /**
   * Identifier referencing an image or packed data
   * @type {number|undefined}
   */
  this.index = undefined;
}
 
/**
 * Typically used with {@link TextureAtlas} to calculate efficient regions of the larger areas to store images or other data. Typically, all units are specified in pixels.
 * @alias TexturePacker
 * @constructor
 * @private
 * @param {options} options Object with the following properties:
 * @param {number} options.width Width of the atlas, in pixels
 * @param {number} options.height Height of atlas, in pixels
 * @param {number} options.borderPadding Amount of border padding, in pixels
 */
function TexturePacker({ width, height, borderPadding }) {
  this._width = width;
  this._height = height;
 
  this._borderPadding = borderPadding;
 
  this._root = new TextureNode({
    x: borderPadding,
    y: borderPadding,
    width: width - 2 * borderPadding,
    height: height - 2 * borderPadding,
  });
}
 
/**
 * Inserts the given object into the next available region based on it's dimensions. Where convenient, it's most efficient to pack items largest to smallest.
 * @private
 * @param {number} index An identifier referencing the image or other stored data
 * @param {TexturePacker.PackableObject} packableObject An object, such as an <code>Image</code>, with <code>width</code> and <code>height</code> properties in pixels.
 * @returns {TextureNode|undefined} The created region, or <code>undefined</code> if there is no region large enough to accommodate the object's dimensions.
 */
TexturePacker.prototype.pack = function (index, { width, height }) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.number.greaterThanOrEquals("index", index, 0);
  Check.typeOf.number.greaterThanOrEquals("image.width", width, 1);
  Check.typeOf.number.greaterThanOrEquals("image.height", height, 1);
  //>>includeEnd('debug');
 
  const node = this._findNode(this._root, { width, height });
  if (!defined(node)) {
    return;
  }
 
  node.index = index;
  return node;
};
 
// A recursive function that finds the best place to insert
// a new image based on existing image 'nodes'.
// Inspired by: http://blackpawn.com/texts/lightmaps/default.html
TexturePacker.prototype._findNode = function (node, { width, height }) {
  if (!defined(node)) {
    return undefined;
  }
 
  // Leaf node
  if (!defined(node.childNode1) && !defined(node.childNode2)) {
    if (defined(node.index)) {
      // Node already contains an image: Skip it.
      return undefined;
    }
 
    const { rectangle } = node;
 
    const nodeWidth = rectangle.width;
    const nodeHeight = rectangle.height;
    const widthDifference = nodeWidth - width;
    const heightDifference = nodeHeight - height;
 
    // Node is smaller than the image.
    if (widthDifference < 0 || heightDifference < 0) {
      return undefined;
    }
 
    // If the node is the same size as the image, return the node
    if (widthDifference === 0 && heightDifference === 0) {
      return node;
    }
 
    const borderPadding = this._borderPadding;
 
    // Vertical split (childNode1 = left half, childNode2 = right half).
    if (widthDifference > heightDifference) {
      node.childNode1 = new TextureNode({
        x: rectangle.x,
        y: rectangle.y,
        width,
        height: nodeHeight,
      });
 
      // Apply padding only along the vertical "cut".
      const widthDifferencePadded = widthDifference - borderPadding;
 
      if (widthDifferencePadded > 0) {
        node.childNode2 = new TextureNode({
          x: rectangle.x + width + borderPadding,
          y: rectangle.y,
          width: widthDifferencePadded,
          height: nodeHeight,
        });
      }
 
      return this._findNode(node.childNode1, { width, height });
    }
 
    // Horizontal split (childNode1 = bottom half, childNode2 = top half).
    node.childNode1 = new TextureNode({
      x: rectangle.x,
      y: rectangle.y,
      width: nodeWidth,
      height,
    });
 
    // Apply padding only along the horizontal "cut".
    const heightDifferencePadded = heightDifference - borderPadding;
 
    if (heightDifferencePadded > 0) {
      node.childNode2 = new TextureNode({
        x: rectangle.x,
        y: rectangle.y + height + borderPadding,
        width: nodeWidth,
        height: heightDifferencePadded,
      });
    }
 
    return this._findNode(node.childNode1, { width, height });
  }
 
  // If not a leaf node
  return (
    this._findNode(node.childNode1, { width, height }) ||
    this._findNode(node.childNode2, { width, height })
  );
};
 
export default TexturePacker;