| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 | import BoundingRectangle from "../Core/BoundingRectangle.js";import Cartesian2 from "../Core/Cartesian2.js";import createGuid from "../Core/createGuid.js";import defaultValue from "../Core/defaultValue.js";import defined from "../Core/defined.js";import destroyObject from "../Core/destroyObject.js";import DeveloperError from "../Core/DeveloperError.js";import PixelFormat from "../Core/PixelFormat.js";import Resource from "../Core/Resource.js";import RuntimeError from "../Core/RuntimeError.js";import Framebuffer from "../Renderer/Framebuffer.js";import Texture from "../Renderer/Texture.js";// The atlas is made up of regions of space called nodes that contain images or child nodes.function TextureAtlasNode(  bottomLeft,  topRight,  childNode1,  childNode2,  imageIndex) {  this.bottomLeft = defaultValue(bottomLeft, Cartesian2.ZERO);  this.topRight = defaultValue(topRight, Cartesian2.ZERO);  this.childNode1 = childNode1;  this.childNode2 = childNode2;  this.imageIndex = imageIndex;}const defaultInitialSize = new Cartesian2(16.0, 16.0);/** * A TextureAtlas stores multiple images in one square texture and keeps * track of the texture coordinates for each image. TextureAtlas is dynamic, * meaning new images can be added at any point in time. * Texture coordinates are subject to change if the texture atlas resizes, so it is * important to check {@link TextureAtlas#getGUID} before using old values. * * @alias TextureAtlas * @constructor * * @param {Object} options Object with the following properties: * @param {Scene} options.context The context in which the texture gets created. * @param {PixelFormat} [options.pixelFormat=PixelFormat.RGBA] The pixel format of the texture. * @param {Number} [options.borderWidthInPixels=1] The amount of spacing between adjacent images in pixels. * @param {Cartesian2} [options.initialSize=new Cartesian2(16.0, 16.0)] The initial side lengths of the texture. * * @exception {DeveloperError} borderWidthInPixels must be greater than or equal to zero. * @exception {DeveloperError} initialSize must be greater than zero. * * @private */function TextureAtlas(options) {  options = defaultValue(options, defaultValue.EMPTY_OBJECT);  const borderWidthInPixels = defaultValue(options.borderWidthInPixels, 1.0);  const initialSize = defaultValue(options.initialSize, defaultInitialSize);  //>>includeStart('debug', pragmas.debug);  if (!defined(options.context)) {    throw new DeveloperError("context is required.");  }  if (borderWidthInPixels < 0) {    throw new DeveloperError(      "borderWidthInPixels must be greater than or equal to zero."    );  }  if (initialSize.x < 1 || initialSize.y < 1) {    throw new DeveloperError("initialSize must be greater than zero.");  }  //>>includeEnd('debug');  this._context = options.context;  this._pixelFormat = defaultValue(options.pixelFormat, PixelFormat.RGBA);  this._borderWidthInPixels = borderWidthInPixels;  this._textureCoordinates = [];  this._guid = createGuid();  this._idHash = {};  this._indexHash = {};  this._initialSize = initialSize;  this._root = undefined;}Object.defineProperties(TextureAtlas.prototype, {  /**   * The amount of spacing between adjacent images in pixels.   * @memberof TextureAtlas.prototype   * @type {Number}   */  borderWidthInPixels: {    get: function () {      return this._borderWidthInPixels;    },  },  /**   * An array of {@link BoundingRectangle} texture coordinate regions for all the images in the texture atlas.   * The x and y values of the rectangle correspond to the bottom-left corner of the texture coordinate.   * The coordinates are in the order that the corresponding images were added to the atlas.   * @memberof TextureAtlas.prototype   * @type {BoundingRectangle[]}   */  textureCoordinates: {    get: function () {      return this._textureCoordinates;    },  },  /**   * The texture that all of the images are being written to.   * @memberof TextureAtlas.prototype   * @type {Texture}   */  texture: {    get: function () {      if (!defined(this._texture)) {        this._texture = new Texture({          context: this._context,          width: this._initialSize.x,          height: this._initialSize.y,          pixelFormat: this._pixelFormat,        });      }      return this._texture;    },  },  /**   * The number of images in the texture atlas. This value increases   * every time addImage or addImages is called.   * Texture coordinates are subject to change if the texture atlas resizes, so it is   * important to check {@link TextureAtlas#getGUID} before using old values.   * @memberof TextureAtlas.prototype   * @type {Number}   */  numberOfImages: {    get: function () {      return this._textureCoordinates.length;    },  },  /**   * The atlas' globally unique identifier (GUID).   * The GUID changes whenever the texture atlas is modified.   * Classes that use a texture atlas should check if the GUID   * has changed before processing the atlas data.   * @memberof TextureAtlas.prototype   * @type {String}   */  guid: {    get: function () {      return this._guid;    },  },});// Builds a larger texture and copies the old texture into the new one.function resizeAtlas(textureAtlas, image) {  const context = textureAtlas._context;  const numImages = textureAtlas.numberOfImages;  const scalingFactor = 2.0;  const borderWidthInPixels = textureAtlas._borderWidthInPixels;  if (numImages > 0) {    const oldAtlasWidth = textureAtlas._texture.width;    const oldAtlasHeight = textureAtlas._texture.height;    const atlasWidth =      scalingFactor * (oldAtlasWidth + image.width + borderWidthInPixels);    const atlasHeight =      scalingFactor * (oldAtlasHeight + image.height + borderWidthInPixels);    const widthRatio = oldAtlasWidth / atlasWidth;    const heightRatio = oldAtlasHeight / atlasHeight;    // Create new node structure, putting the old root node in the bottom left.    const nodeBottomRight = new TextureAtlasNode(      new Cartesian2(oldAtlasWidth + borderWidthInPixels, borderWidthInPixels),      new Cartesian2(atlasWidth, oldAtlasHeight)    );    const nodeBottomHalf = new TextureAtlasNode(      new Cartesian2(),      new Cartesian2(atlasWidth, oldAtlasHeight),      textureAtlas._root,      nodeBottomRight    );    const nodeTopHalf = new TextureAtlasNode(      new Cartesian2(borderWidthInPixels, oldAtlasHeight + borderWidthInPixels),      new Cartesian2(atlasWidth, atlasHeight)    );    const nodeMain = new TextureAtlasNode(      new Cartesian2(),      new Cartesian2(atlasWidth, atlasHeight),      nodeBottomHalf,      nodeTopHalf    );    // Resize texture coordinates.    for (let i = 0; i < textureAtlas._textureCoordinates.length; i++) {      const texCoord = textureAtlas._textureCoordinates[i];      if (defined(texCoord)) {        texCoord.x *= widthRatio;        texCoord.y *= heightRatio;        texCoord.width *= widthRatio;        texCoord.height *= heightRatio;      }    }    // Copy larger texture.    const newTexture = new Texture({      context: textureAtlas._context,      width: atlasWidth,      height: atlasHeight,      pixelFormat: textureAtlas._pixelFormat,    });    const framebuffer = new Framebuffer({      context: context,      colorTextures: [textureAtlas._texture],      destroyAttachments: false,    });    framebuffer._bind();    newTexture.copyFromFramebuffer(0, 0, 0, 0, atlasWidth, atlasHeight);    framebuffer._unBind();    framebuffer.destroy();    textureAtlas._texture =      textureAtlas._texture && textureAtlas._texture.destroy();    textureAtlas._texture = newTexture;    textureAtlas._root = nodeMain;  } else {    // First image exceeds initialSize    let initialWidth = scalingFactor * (image.width + 2 * borderWidthInPixels);    let initialHeight =      scalingFactor * (image.height + 2 * borderWidthInPixels);    if (initialWidth < textureAtlas._initialSize.x) {      initialWidth = textureAtlas._initialSize.x;    }    if (initialHeight < textureAtlas._initialSize.y) {      initialHeight = textureAtlas._initialSize.y;    }    textureAtlas._texture =      textureAtlas._texture && textureAtlas._texture.destroy();    textureAtlas._texture = new Texture({      context: textureAtlas._context,      width: initialWidth,      height: initialHeight,      pixelFormat: textureAtlas._pixelFormat,    });    textureAtlas._root = new TextureAtlasNode(      new Cartesian2(borderWidthInPixels, borderWidthInPixels),      new Cartesian2(initialWidth, initialHeight)    );  }}// 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.htmlfunction findNode(textureAtlas, node, image) {  if (!defined(node)) {    return undefined;  }  // If a leaf node  if (!defined(node.childNode1) && !defined(node.childNode2)) {    // Node already contains an image, don't add to it.    if (defined(node.imageIndex)) {      return undefined;    }    const nodeWidth = node.topRight.x - node.bottomLeft.x;    const nodeHeight = node.topRight.y - node.bottomLeft.y;    const widthDifference = nodeWidth - image.width;    const heightDifference = nodeHeight - image.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;    }    // Vertical split (childNode1 = left half, childNode2 = right half).    if (widthDifference > heightDifference) {      node.childNode1 = new TextureAtlasNode(        new Cartesian2(node.bottomLeft.x, node.bottomLeft.y),        new Cartesian2(node.bottomLeft.x + image.width, node.topRight.y)      );      // Only make a second child if the border gives enough space.      const childNode2BottomLeftX =        node.bottomLeft.x + image.width + textureAtlas._borderWidthInPixels;      if (childNode2BottomLeftX < node.topRight.x) {        node.childNode2 = new TextureAtlasNode(          new Cartesian2(childNode2BottomLeftX, node.bottomLeft.y),          new Cartesian2(node.topRight.x, node.topRight.y)        );      }    }    // Horizontal split (childNode1 = bottom half, childNode2 = top half).    else {      node.childNode1 = new TextureAtlasNode(        new Cartesian2(node.bottomLeft.x, node.bottomLeft.y),        new Cartesian2(node.topRight.x, node.bottomLeft.y + image.height)      );      // Only make a second child if the border gives enough space.      const childNode2BottomLeftY =        node.bottomLeft.y + image.height + textureAtlas._borderWidthInPixels;      if (childNode2BottomLeftY < node.topRight.y) {        node.childNode2 = new TextureAtlasNode(          new Cartesian2(node.bottomLeft.x, childNode2BottomLeftY),          new Cartesian2(node.topRight.x, node.topRight.y)        );      }    }    return findNode(textureAtlas, node.childNode1, image);  }  // If not a leaf node  return (    findNode(textureAtlas, node.childNode1, image) ||    findNode(textureAtlas, node.childNode2, image)  );}// Adds image of given index to the texture atlas. Called from addImage and addImages.function addImage(textureAtlas, image, index) {  const node = findNode(textureAtlas, textureAtlas._root, image);  if (defined(node)) {    // Found a node that can hold the image.    node.imageIndex = index;    // Add texture coordinate and write to texture    const atlasWidth = textureAtlas._texture.width;    const atlasHeight = textureAtlas._texture.height;    const nodeWidth = node.topRight.x - node.bottomLeft.x;    const nodeHeight = node.topRight.y - node.bottomLeft.y;    const x = node.bottomLeft.x / atlasWidth;    const y = node.bottomLeft.y / atlasHeight;    const w = nodeWidth / atlasWidth;    const h = nodeHeight / atlasHeight;    textureAtlas._textureCoordinates[index] = new BoundingRectangle(x, y, w, h);    textureAtlas._texture.copyFrom({      source: image,      xOffset: node.bottomLeft.x,      yOffset: node.bottomLeft.y,    });  } else {    // No node found, must resize the texture atlas.    resizeAtlas(textureAtlas, image);    addImage(textureAtlas, image, index);  }  textureAtlas._guid = createGuid();}function getIndex(atlas, image) {  if (!defined(atlas) || atlas.isDestroyed()) {    return -1;  }  const index = atlas.numberOfImages;  addImage(atlas, image, index);  return index;}/** * If the image is already in the atlas, the existing index is returned. Otherwise, the result is undefined. * * @param {String} id An identifier to detect whether the image already exists in the atlas. * @returns {Number|undefined} The image index, or undefined if the image does not exist in the atlas. */TextureAtlas.prototype.getImageIndex = function (id) {  //>>includeStart('debug', pragmas.debug);  if (!defined(id)) {    throw new DeveloperError("id is required.");  }  //>>includeEnd('debug');  return this._indexHash[id];};/** * Adds an image to the atlas synchronously.  If the image is already in the atlas, the atlas is unchanged and * the existing index is used. * * @param {String} id An identifier to detect whether the image already exists in the atlas. * @param {HTMLImageElement|HTMLCanvasElement} image An image or canvas to add to the texture atlas. * @returns {Number} The image index. */TextureAtlas.prototype.addImageSync = function (id, image) {  //>>includeStart('debug', pragmas.debug);  if (!defined(id)) {    throw new DeveloperError("id is required.");  }  if (!defined(image)) {    throw new DeveloperError("image is required.");  }  //>>includeEnd('debug');  let index = this._indexHash[id];  if (defined(index)) {    // we're already aware of this source    return index;  }  index = getIndex(this, image);  // store the promise  this._idHash[id] = Promise.resolve(index);  this._indexHash[id] = index;  // but return the value synchronously  return index;};/** * Adds an image to the atlas.  If the image is already in the atlas, the atlas is unchanged and * the existing index is used. * * @param {String} id An identifier to detect whether the image already exists in the atlas. * @param {HTMLImageElement|HTMLCanvasElement|String|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas, *        or a URL to an Image, or a Promise for an image, or a function that creates an image. * @returns {Promise.<Number>} A Promise for the image index. */TextureAtlas.prototype.addImage = function (id, image) {  //>>includeStart('debug', pragmas.debug);  if (!defined(id)) {    throw new DeveloperError("id is required.");  }  if (!defined(image)) {    throw new DeveloperError("image is required.");  }  //>>includeEnd('debug');  let indexPromise = this._idHash[id];  if (defined(indexPromise)) {    // we're already aware of this source    return indexPromise;  }  // not in atlas, create the promise for the index  if (typeof image === "function") {    // if image is a function, call it    image = image(id);    //>>includeStart('debug', pragmas.debug);    if (!defined(image)) {      throw new DeveloperError("image is required.");    }    //>>includeEnd('debug');  } else if (typeof image === "string" || image instanceof Resource) {    // Get a resource    const resource = Resource.createIfNeeded(image);    image = resource.fetchImage();  }  const that = this;  indexPromise = Promise.resolve(image).then(function (image) {    const index = getIndex(that, image);    that._indexHash[id] = index;    return index;  });  // store the promise  this._idHash[id] = indexPromise;  return indexPromise;};/** * Add a sub-region of an existing atlas image as additional image indices. * * @param {String} id The identifier of the existing image. * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} sub-region measured in pixels from the bottom-left. * * @returns {Promise.<Number>} A Promise for the image index. */TextureAtlas.prototype.addSubRegion = function (id, subRegion) {  //>>includeStart('debug', pragmas.debug);  if (!defined(id)) {    throw new DeveloperError("id is required.");  }  if (!defined(subRegion)) {    throw new DeveloperError("subRegion is required.");  }  //>>includeEnd('debug');  const indexPromise = this._idHash[id];  if (!defined(indexPromise)) {    throw new RuntimeError(`image with id "${id}" not found in the atlas.`);  }  const that = this;  return Promise.resolve(indexPromise).then(function (index) {    if (index === -1) {      // the atlas is destroyed      return -1;    }    const atlasWidth = that._texture.width;    const atlasHeight = that._texture.height;    const numImages = that.numberOfImages;    const baseRegion = that._textureCoordinates[index];    const x = baseRegion.x + subRegion.x / atlasWidth;    const y = baseRegion.y + subRegion.y / atlasHeight;    const w = subRegion.width / atlasWidth;    const h = subRegion.height / atlasHeight;    that._textureCoordinates.push(new BoundingRectangle(x, y, w, h));    that._guid = createGuid();    return numImages;  });};/** * Returns true if this object was destroyed; otherwise, false. * <br /><br /> * If this object was destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. * * @returns {Boolean} True if this object was destroyed; otherwise, false. * * @see TextureAtlas#destroy */TextureAtlas.prototype.isDestroyed = function () {  return false;};/** * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic * release of WebGL resources, instead of relying on the garbage collector to destroy this object. * <br /><br /> * Once an object is destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore, * assign the return value (<code>undefined</code>) to the object as done in the example. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * atlas = atlas && atlas.destroy(); * * @see TextureAtlas#isDestroyed */TextureAtlas.prototype.destroy = function () {  this._texture = this._texture && this._texture.destroy();  return destroyObject(this);};/** * A function that creates an image. * @callback TextureAtlas.CreateImageCallback * @param {String} id The identifier of the image to load. * @returns {HTMLImageElement|Promise<HTMLImageElement>} The image, or a promise that will resolve to an image. */export default TextureAtlas;
 |