智慧水务管理系统 - 精河县供水工程综合管理平台

TextureAtlas.js 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. import BoundingRectangle from "../Core/BoundingRectangle.js";
  2. import Cartesian2 from "../Core/Cartesian2.js";
  3. import Check from "../Core/Check.js";
  4. import createGuid from "../Core/createGuid.js";
  5. import Frozen from "../Core/Frozen.js";
  6. import defined from "../Core/defined.js";
  7. import destroyObject from "../Core/destroyObject.js";
  8. import CesiumMath from "../Core/Math.js";
  9. import PixelFormat from "../Core/PixelFormat.js";
  10. import Resource from "../Core/Resource.js";
  11. import RuntimeError from "../Core/RuntimeError.js";
  12. import TexturePacker from "../Core/TexturePacker.js";
  13. import Framebuffer from "./Framebuffer.js";
  14. import Texture from "./Texture.js";
  15. const defaultInitialDimensions = 16;
  16. /**
  17. * A TextureAtlas stores multiple images in one∂ texture and keeps
  18. * track of the texture coordinates for each image. A TextureAtlas is dynamic,
  19. * meaning new images can be added at any point in time.
  20. * Texture coordinates are subject to change if the texture atlas resizes, so it's
  21. * important to check {@link TextureAtlas#guid} before using old values.
  22. *
  23. * @alias TextureAtlas
  24. * @constructor
  25. *
  26. * @param {object} options Object with the following properties:
  27. * @param {PixelFormat} [options.pixelFormat=PixelFormat.RGBA] The pixel format of the texture.
  28. * @param {Sampler} [options.sampler=new Sampler()] Information about how to sample the texture.
  29. * @param {number} [options.borderWidthInPixels=1] The amount of spacing between adjacent images in pixels.
  30. * @param {Cartesian2} [options.initialSize=new Cartesian2(16.0, 16.0)] The initial side lengths of the texture.
  31. *
  32. * @exception {DeveloperError} borderWidthInPixels must be greater than or equal to zero.
  33. * @exception {DeveloperError} initialSize must be greater than zero.
  34. *
  35. * @private
  36. */
  37. function TextureAtlas(options) {
  38. options = options ?? Frozen.EMPTY_OBJECT;
  39. const borderWidthInPixels = options.borderWidthInPixels ?? 1.0;
  40. const initialSize =
  41. options.initialSize ??
  42. new Cartesian2(defaultInitialDimensions, defaultInitialDimensions);
  43. //>>includeStart('debug', pragmas.debug);
  44. Check.typeOf.number.greaterThanOrEquals(
  45. "options.borderWidthInPixels",
  46. borderWidthInPixels,
  47. 0,
  48. );
  49. Check.typeOf.number.greaterThan("options.initialSize.x", initialSize.x, 0);
  50. Check.typeOf.number.greaterThan("options.initialSize.y", initialSize.y, 0);
  51. //>>includeEnd('debug');
  52. this._pixelFormat = options.pixelFormat ?? PixelFormat.RGBA;
  53. this._sampler = options.sampler;
  54. this._borderWidthInPixels = borderWidthInPixels;
  55. this._initialSize = initialSize;
  56. this._texturePacker = undefined;
  57. /** @type {BoundingRectangle[]} */
  58. this._rectangles = [];
  59. /** @type {Map<number, number>} */
  60. this._subRegions = new Map();
  61. this._guid = createGuid();
  62. this._imagesToAddQueue = [];
  63. /** @type {Map<string, number>} */
  64. this._indexById = new Map();
  65. /** @type {Map<string, Promise<number>>} */
  66. this._indexPromiseById = new Map();
  67. this._nextIndex = 0;
  68. }
  69. Object.defineProperties(TextureAtlas.prototype, {
  70. /**
  71. * The amount of spacing between adjacent images in pixels.
  72. * @memberof TextureAtlas.prototype
  73. * @type {number}
  74. * @readonly
  75. * @private
  76. */
  77. borderWidthInPixels: {
  78. get: function () {
  79. return this._borderWidthInPixels;
  80. },
  81. },
  82. /**
  83. * An array of {@link BoundingRectangle} pixel offset and dimensions for all the images in the texture atlas.
  84. * The x and y values of the rectangle correspond to the bottom-left corner of the texture coordinate.
  85. * If the index is a subregion of an existing image, thea and y values are specified as offsets relative to the parent.
  86. * The coordinates are in the order that the corresponding images were added to the atlas.
  87. * @memberof TextureAtlas.prototype
  88. * @type {BoundingRectangle[]}
  89. * @readonly
  90. * @private
  91. */
  92. rectangles: {
  93. get: function () {
  94. return this._rectangles;
  95. },
  96. },
  97. /**
  98. * The texture that all of the images are being written to. The value will be <code>undefined</code> until the first update.
  99. * @memberof TextureAtlas.prototype
  100. * @type {Texture|undefined}
  101. * @readonly
  102. * @private
  103. */
  104. texture: {
  105. get: function () {
  106. return this._texture;
  107. },
  108. },
  109. /**
  110. * The pixel format of the texture.
  111. * @memberof TextureAtlas.prototype
  112. * @type {PixelFormat}
  113. * @readonly
  114. * @private
  115. */
  116. pixelFormat: {
  117. get: function () {
  118. return this._pixelFormat;
  119. },
  120. },
  121. /**
  122. * The sampler to use when sampling this texture. If <code>undefined</code>, the default sampler is used.
  123. * @memberof TextureAtlas.prototype
  124. * @type {Sampler|undefined}
  125. * @readonly
  126. * @private
  127. */
  128. sampler: {
  129. get: function () {
  130. return this._sampler;
  131. },
  132. },
  133. /**
  134. * The number of images in the texture atlas. This value increases
  135. * every time addImage or addImageSubRegion is called.
  136. * Texture coordinates are subject to change if the texture atlas resizes, so it is
  137. * important to check {@link TextureAtlas#guid} before using old values.
  138. * @memberof TextureAtlas.prototype
  139. * @type {number}
  140. * @readonly
  141. * @private
  142. */
  143. numberOfImages: {
  144. get: function () {
  145. return this._nextIndex;
  146. },
  147. },
  148. /**
  149. * The atlas' globally unique identifier (GUID).
  150. * The GUID changes whenever the texture atlas is modified.
  151. * Classes that use a texture atlas should check if the GUID
  152. * has changed before processing the atlas data.
  153. * @memberof TextureAtlas.prototype
  154. * @type {string}
  155. * @readonly
  156. * @private
  157. */
  158. guid: {
  159. get: function () {
  160. return this._guid;
  161. },
  162. },
  163. /**
  164. * Returns the size in bytes of the texture.
  165. * @memberof TextureAtlas.prototype
  166. * @type {number}
  167. * @readonly
  168. * @private
  169. */
  170. sizeInBytes: {
  171. get: function () {
  172. if (!defined(this._texture)) {
  173. return 0;
  174. }
  175. return this._texture.sizeInBytes;
  176. },
  177. },
  178. });
  179. /**
  180. * Get the texture coordinates for reading the associated image in shaders.
  181. * @param {number} index The index of the image region.
  182. * @param {BoundingRectangle} [result] The object into which to store the result.
  183. * @return {BoundingRectangle} The modified result parameter or a new BoundingRectangle instance if one was not provided.
  184. * @private
  185. * @example
  186. * const index = await atlas.addImage("myImage", image);
  187. * const rectangle = atlas.computeTextureCoordinates(index);
  188. * BoundingRectangle.pack(rectangle, bufferView);
  189. */
  190. TextureAtlas.prototype.computeTextureCoordinates = function (index, result) {
  191. //>>includeStart('debug', pragmas.debug);
  192. Check.typeOf.number.greaterThanOrEquals("index", index, 0);
  193. //>>includeEnd('debug');
  194. const texture = this._texture;
  195. const rectangle = this._rectangles[index];
  196. if (!defined(result)) {
  197. result = new BoundingRectangle();
  198. }
  199. if (!defined(rectangle)) {
  200. result.x = 0;
  201. result.y = 0;
  202. result.width = 0;
  203. result.height = 0;
  204. return result;
  205. }
  206. const atlasWidth = texture.width;
  207. const atlasHeight = texture.height;
  208. const width = rectangle.width;
  209. const height = rectangle.height;
  210. let x = rectangle.x;
  211. let y = rectangle.y;
  212. const parentIndex = this._subRegions.get(index);
  213. if (defined(parentIndex)) {
  214. const parentRectangle = this._rectangles[parentIndex];
  215. x += parentRectangle.x;
  216. y += parentRectangle.y;
  217. }
  218. result.x = x / atlasWidth;
  219. result.y = y / atlasHeight;
  220. result.width = width / atlasWidth;
  221. result.height = height / atlasHeight;
  222. return result;
  223. };
  224. /**
  225. * Perform a WebGL texture copy for each existing image from its previous packed position to its new packed position in the new texture.
  226. * @param {Context} context The rendering context
  227. * @param {number} width The pixel width of the texture
  228. * @param {number} height The pixel height of the texture
  229. * @param {BoundingRectangle[]} rectangles The packed bounding rectangles for the reszied texture
  230. * @param {number} queueOffset Index of the last queued item that was successfully packed
  231. * @private
  232. */
  233. TextureAtlas.prototype._copyFromTexture = function (
  234. context,
  235. width,
  236. height,
  237. rectangles,
  238. ) {
  239. const pixelFormat = this._pixelFormat;
  240. const sampler = this._sampler;
  241. const newTexture = new Texture({
  242. context,
  243. height,
  244. width,
  245. pixelFormat,
  246. sampler,
  247. });
  248. const gl = context._gl;
  249. const target = newTexture._textureTarget;
  250. const oldTexture = this._texture;
  251. const framebuffer = new Framebuffer({
  252. context,
  253. colorTextures: [oldTexture],
  254. destroyAttachments: false,
  255. });
  256. gl.activeTexture(gl.TEXTURE0);
  257. gl.bindTexture(target, newTexture._texture);
  258. framebuffer._bind();
  259. // Copy any textures from the old atlas to its new position in the new atlas
  260. const oldRectangles = this.rectangles;
  261. const subRegions = this._subRegions;
  262. for (let index = 0; index < oldRectangles.length; ++index) {
  263. const rectangle = rectangles[index];
  264. const frameBufferOffset = oldRectangles[index];
  265. if (
  266. !defined(rectangle) ||
  267. !defined(frameBufferOffset) ||
  268. defined(subRegions.get(index)) // The rectangle corresponds to a subregion of a parent image
  269. ) {
  270. continue;
  271. }
  272. const { x, y, width, height } = rectangle;
  273. gl.copyTexSubImage2D(
  274. target,
  275. 0,
  276. x,
  277. y,
  278. frameBufferOffset.x,
  279. frameBufferOffset.y,
  280. width,
  281. height,
  282. );
  283. }
  284. gl.bindTexture(target, null);
  285. newTexture._initialized = true;
  286. framebuffer._unBind();
  287. framebuffer.destroy();
  288. return newTexture;
  289. };
  290. /**
  291. * Recreates the texture atlas texture with new dimensions and repacks images as needed.
  292. * @param {Context} context The rendering context
  293. * @param {number} [queueOffset = 0] Index of the last queued item that was successfully packed
  294. * @private
  295. */
  296. TextureAtlas.prototype._resize = function (context, queueOffset = 0) {
  297. const borderPadding = this._borderWidthInPixels;
  298. const oldRectangles = this._rectangles;
  299. const queue = this._imagesToAddQueue;
  300. const oldTexture = this._texture;
  301. let width = oldTexture.width;
  302. let height = oldTexture.height;
  303. // Get the rectangles (width and height) of the current set of images,
  304. // ignoring the subregions, which don't get packed
  305. const subRegions = this._subRegions;
  306. const toPack = oldRectangles
  307. .map((image, index) => {
  308. return new AddImageRequest({ index, image });
  309. })
  310. .filter(
  311. (request, index) =>
  312. defined(request.image) && !defined(subRegions.get(index)),
  313. );
  314. // Add the new set of images
  315. let maxWidth = 0;
  316. let maxHeight = 0;
  317. let areaQueued = 0;
  318. for (let i = queueOffset; i < queue.length; ++i) {
  319. const { width, height } = queue[i].image;
  320. maxWidth = Math.max(maxWidth, width);
  321. maxHeight = Math.max(maxHeight, height);
  322. areaQueued += width * height;
  323. toPack.push(queue[i]);
  324. }
  325. // At minimum, atlas must fit its largest input images. Texture coordinates are
  326. // compressed to 0–1 with 12-bit precision, so use power-of-two size to align pixels.
  327. width = CesiumMath.nextPowerOfTwo(Math.max(maxWidth, width));
  328. height = CesiumMath.nextPowerOfTwo(Math.max(maxHeight, height));
  329. // Iteratively double the smallest dimension until atlas area is (approximately) sufficient.
  330. while (areaQueued >= width * height) {
  331. if (width > height) {
  332. height *= 2;
  333. } else {
  334. width *= 2;
  335. }
  336. }
  337. toPack.sort(
  338. ({ image: imageA }, { image: imageB }) =>
  339. imageB.height * imageB.width - imageA.height * imageA.width,
  340. );
  341. const newRectangles = new Array(this._nextIndex);
  342. for (const index of this._subRegions.keys()) {
  343. // Subregions are specified relative to their parents,
  344. // so we can copy them directly
  345. if (defined(subRegions.get(index))) {
  346. newRectangles[index] = oldRectangles[index];
  347. }
  348. }
  349. let texturePacker,
  350. packed = false;
  351. while (!packed) {
  352. texturePacker = new TexturePacker({ height, width, borderPadding });
  353. let i;
  354. for (i = 0; i < toPack.length; ++i) {
  355. const { index, image } = toPack[i];
  356. if (!defined(image)) {
  357. continue;
  358. }
  359. const repackedNode = texturePacker.pack(index, image);
  360. if (!defined(repackedNode)) {
  361. // Could not fit everything into the new texture.
  362. // Scale texture size and try again
  363. if (width > height) {
  364. // Resize height
  365. height *= 2.0;
  366. } else {
  367. // Resize width
  368. width *= 2.0;
  369. }
  370. break;
  371. }
  372. newRectangles[index] = repackedNode.rectangle;
  373. }
  374. packed = i === toPack.length;
  375. }
  376. this._texturePacker = texturePacker;
  377. this._texture = this._copyFromTexture(context, width, height, newRectangles);
  378. oldTexture.destroy();
  379. this._rectangles = newRectangles;
  380. this._guid = createGuid();
  381. };
  382. /**
  383. * Return the index of the image region for the specified ID. If the image is already in the atlas, the existing index is returned. Otherwise, the result is undefined.
  384. * @param {string} id An identifier to detect whether the image already exists in the atlas.
  385. * @returns {number|undefined} The image index, or undefined if the image does not exist in the atlas.
  386. * @private
  387. */
  388. TextureAtlas.prototype.getImageIndex = function (id) {
  389. //>>includeStart('debug', pragmas.debug);
  390. Check.typeOf.string("id", id);
  391. //>>includeEnd('debug');
  392. return this._indexById.get(id);
  393. };
  394. /**
  395. * Copy image data into the underlying texture atlas.
  396. * @param {AddImageRequest} imageRequest The data needed to resolve the call to addImage in the queue
  397. * @private
  398. */
  399. TextureAtlas.prototype._copyImageToTexture = function ({
  400. index,
  401. image,
  402. resolve,
  403. reject,
  404. }) {
  405. const texture = this._texture;
  406. const rectangle = this._rectangles[index];
  407. try {
  408. texture.copyFrom({
  409. source: image,
  410. xOffset: rectangle.x,
  411. yOffset: rectangle.y,
  412. });
  413. if (defined(resolve)) {
  414. resolve(index);
  415. }
  416. } catch (e) {
  417. if (defined(reject)) {
  418. reject(e);
  419. return;
  420. }
  421. }
  422. };
  423. /**
  424. * Info needed to add a queued image to the texture atlas when update operatons are executed, typically at the end of a frame.
  425. * @constructor
  426. * @private
  427. * @param {object} options Object with the following properties:
  428. * @param {number} options.index An identifier
  429. * @param {TexturePacker.PackableObject} options.image An object, such as an <code>Image</code> with <code>width</code> and <code>height</code> properties in pixels
  430. * @param {function} [options.resolve] The promise resolver
  431. * @param {function} [options.reject] The promise rejecter
  432. */
  433. function AddImageRequest({ index, image, resolve, reject }) {
  434. this.index = index;
  435. this.image = image;
  436. this.resolve = resolve;
  437. this.reject = reject;
  438. this.rectangle = undefined;
  439. }
  440. /**
  441. * Adds an image to the queue for this frame.
  442. * The image will be copied to the texture at the end of the frame, resizing the texture if needed.
  443. *
  444. * @private
  445. * @param {number} index An identifier
  446. * @param {TexturePacker.PackableObject} image An object, such as an <code>Image</code> with <code>width</code> and <code>height</code> properties in pixels
  447. * @returns {Promise<number>} Promise which resolves to the image index once the image has been added, or rejects if there was an error. The promise resolves to <code>-1</code> if the texture atlas is destoyed in the interim.
  448. */
  449. TextureAtlas.prototype._addImage = function (index, image) {
  450. //>>includeStart('debug', pragmas.debug);
  451. Check.typeOf.number.greaterThanOrEquals("index", index, 0);
  452. Check.defined("image", image);
  453. //>>includeEnd('debug');
  454. return new Promise((resolve, reject) => {
  455. this._imagesToAddQueue.push(
  456. new AddImageRequest({
  457. index,
  458. image,
  459. resolve,
  460. reject,
  461. }),
  462. );
  463. this._imagesToAddQueue.sort(
  464. ({ image: imageA }, { image: imageB }) =>
  465. imageB.height * imageB.width - imageA.height * imageA.width,
  466. );
  467. });
  468. };
  469. /**
  470. * Process the image queue for this frame, copying to the texture atlas and resizing the texture as needed.
  471. * @private
  472. * @param {Context} context The rendering context
  473. * @return {boolean} true if the texture was updated this frame
  474. */
  475. TextureAtlas.prototype._processImageQueue = function (context) {
  476. const queue = this._imagesToAddQueue;
  477. if (queue.length === 0) {
  478. return false;
  479. }
  480. this._rectangles.length = this._nextIndex;
  481. let i, error;
  482. for (i = 0; i < queue.length; ++i) {
  483. const imageRequest = queue[i];
  484. const { image, index } = imageRequest;
  485. const node = this._texturePacker.pack(index, image);
  486. if (!defined(node)) {
  487. // Atlas cannot fit all images in the queue
  488. // Bail early and resize
  489. try {
  490. this._resize(context, i);
  491. } catch (e) {
  492. error = e;
  493. if (defined(imageRequest.reject)) {
  494. imageRequest.reject(error);
  495. }
  496. }
  497. break;
  498. }
  499. this._rectangles[index] = node.rectangle;
  500. }
  501. if (defined(error)) {
  502. for (i = i + 1; i < queue.length; ++i) {
  503. const { resolve } = queue[i];
  504. if (defined(resolve)) {
  505. resolve(-1);
  506. }
  507. }
  508. queue.length = 0;
  509. return false;
  510. }
  511. for (let i = 0; i < queue.length; ++i) {
  512. this._copyImageToTexture(queue[i]);
  513. }
  514. queue.length = 0;
  515. return true;
  516. };
  517. /**
  518. * Processes any updates queued this frame, and updates rendering resources accordingly. Call before or after a frame has been rendered to avoid any race conditions for any dependant render commands.
  519. * @private
  520. * @param {Context} context The rendering context
  521. * @return {boolean} true if rendering resources were updated.
  522. */
  523. TextureAtlas.prototype.update = function (context) {
  524. if (!defined(this._texture)) {
  525. const width = this._initialSize.x;
  526. const height = this._initialSize.y;
  527. const pixelFormat = this._pixelFormat;
  528. const sampler = this._sampler;
  529. const borderPadding = this._borderWidthInPixels;
  530. this._texture = new Texture({
  531. context,
  532. width,
  533. height,
  534. pixelFormat,
  535. sampler,
  536. });
  537. this._texturePacker = new TexturePacker({
  538. height,
  539. width,
  540. borderPadding,
  541. });
  542. }
  543. return this._processImageQueue(context);
  544. };
  545. async function resolveImage(image, id) {
  546. if (typeof image === "function") {
  547. image = image(id);
  548. }
  549. if (typeof image === "string" || image instanceof Resource) {
  550. // Fetch the resource
  551. const resource = Resource.createIfNeeded(image);
  552. image = resource.fetchImage();
  553. }
  554. return image;
  555. }
  556. /**
  557. * Adds an image to the atlas. If the image is already in the atlas, the atlas is unchanged and
  558. * the existing index is used.
  559. * @private
  560. * @param {string} id An identifier to detect whether the image already exists in the atlas.
  561. * @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
  562. * or a URL to an Image, or a Promise for an image, or a function that creates an image.
  563. * @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
  564. * @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
  565. * @returns {Promise<number> | number} The image region index or a promise that resolves to it. -1 is returned if resources are in the process of being destroyed.
  566. */
  567. TextureAtlas.prototype.addImage = function (id, image, width, height) {
  568. //>>includeStart('debug', pragmas.debug);
  569. Check.typeOf.string("id", id);
  570. Check.defined("image", image);
  571. //>>includeEnd('debug');
  572. let promise = this._indexPromiseById.get(id);
  573. let index = this._indexById.get(id);
  574. if (defined(promise)) {
  575. // This image is already being added
  576. return promise;
  577. }
  578. if (defined(index)) {
  579. // This image has already been added and resolved
  580. return index;
  581. }
  582. index = this._nextIndex++;
  583. this._indexById.set(id, index);
  584. const resolveAndAddImage = async () => {
  585. const resolvedImage = await resolveImage(image, id);
  586. //>>includeStart('debug', pragmas.debug);
  587. Check.defined("image", resolvedImage);
  588. //>>includeEnd('debug');
  589. if (this.isDestroyed() || !defined(resolvedImage)) {
  590. this._indexPromiseById.delete(id);
  591. return -1;
  592. }
  593. if (defined(width)) {
  594. resolvedImage.width = width;
  595. }
  596. if (defined(height)) {
  597. resolvedImage.height = height;
  598. }
  599. const imageIndex = await this._addImage(index, resolvedImage);
  600. this._indexPromiseById.delete(id);
  601. return imageIndex;
  602. };
  603. promise = resolveAndAddImage();
  604. this._indexPromiseById.set(id, promise);
  605. return promise;
  606. };
  607. /**
  608. * Get an existing sub-region of an existing atlas image as additional image indices.
  609. * @private
  610. * @param {string} id The identifier of the existing image.
  611. * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
  612. * @param {number} imageIndex The index of the image.
  613. * @returns {Promise<number> | number | undefined} The existing subRegion index, or undefined if not yet added.
  614. */
  615. TextureAtlas.prototype.getCachedImageSubRegion = function (
  616. id,
  617. subRegion,
  618. imageIndex,
  619. ) {
  620. const imagePromise = this._indexPromiseById.get(id);
  621. for (const [index, parentIndex] of this._subRegions.entries()) {
  622. if (imageIndex === parentIndex) {
  623. const boundingRegion = this._rectangles[index];
  624. if (boundingRegion.equals(subRegion)) {
  625. // The subregion is already being tracked
  626. if (imagePromise) {
  627. return imagePromise.then((resolvedImageIndex) =>
  628. resolvedImageIndex === -1 ? -1 : index,
  629. );
  630. }
  631. return index;
  632. }
  633. }
  634. }
  635. };
  636. /**
  637. * Add a sub-region of an existing atlas image as additional image indices.
  638. * @private
  639. * @param {string} id The identifier of the existing image.
  640. * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
  641. * @returns {number | Promise<number>} The resolved image region index, or a Promise that resolves to it. -1 is returned if resources are in the process of being destroyed.
  642. */
  643. TextureAtlas.prototype.addImageSubRegion = function (id, subRegion) {
  644. //>>includeStart('debug', pragmas.debug);
  645. Check.typeOf.string("id", id);
  646. Check.defined("subRegion", subRegion);
  647. //>>includeEnd('debug');
  648. const imageIndex = this._indexById.get(id);
  649. if (!defined(imageIndex)) {
  650. throw new RuntimeError(`image with id "${id}" not found in the atlas.`);
  651. }
  652. let index = this.getCachedImageSubRegion(id, subRegion, imageIndex);
  653. if (defined(index)) {
  654. return index;
  655. }
  656. index = this._nextIndex++;
  657. this._subRegions.set(index, imageIndex);
  658. this._rectangles[index] = subRegion.clone();
  659. const indexPromise =
  660. this._indexPromiseById.get(id) ?? Promise.resolve(imageIndex);
  661. return indexPromise.then((imageIndex) => {
  662. if (imageIndex === -1) {
  663. // The atlas has been destroyed
  664. return -1;
  665. }
  666. const rectangle = this._rectangles[imageIndex];
  667. //>>includeStart('debug', pragmas.debug);
  668. Check.typeOf.number.lessThanOrEquals(
  669. "subRegion.x",
  670. subRegion.x,
  671. rectangle.width,
  672. );
  673. Check.typeOf.number.lessThanOrEquals(
  674. "subRegion.x + subRegion.width",
  675. subRegion.x + subRegion.width,
  676. rectangle.width,
  677. );
  678. Check.typeOf.number.lessThanOrEquals(
  679. "subRegion.y",
  680. subRegion.y,
  681. rectangle.height,
  682. );
  683. Check.typeOf.number.lessThanOrEquals(
  684. "subRegion.y + subRegion.height",
  685. subRegion.y + subRegion.height,
  686. rectangle.height,
  687. );
  688. //>>includeEnd('debug');
  689. return index;
  690. });
  691. };
  692. /**
  693. * Returns true if this object was destroyed; otherwise, false.
  694. * <br /><br />
  695. * If this object was destroyed, it should not be used; calling any function other than
  696. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  697. * @private
  698. * @returns {boolean} True if this object was destroyed; otherwise, false.
  699. * @see TextureAtlas#destroy
  700. */
  701. TextureAtlas.prototype.isDestroyed = function () {
  702. return false;
  703. };
  704. /**
  705. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  706. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  707. * <br /><br />
  708. * Once an object is destroyed, it should not be used; calling any function other than
  709. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  710. * assign the return value (<code>undefined</code>) to the object as done in the example.
  711. * @private
  712. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  713. * @example
  714. * atlas = atlas && atlas.destroy();
  715. * @see TextureAtlas#isDestroyed
  716. */
  717. TextureAtlas.prototype.destroy = function () {
  718. this._texture = this._texture && this._texture.destroy();
  719. this._imagesToAddQueue.forEach(({ resolve }) => {
  720. if (defined(resolve)) {
  721. resolve(-1);
  722. }
  723. });
  724. return destroyObject(this);
  725. };
  726. /**
  727. * A function that creates an image.
  728. * @private
  729. * @callback TextureAtlas.CreateImageCallback
  730. * @param {string} id The identifier of the image to load.
  731. * @returns {HTMLImageElement|Promise<HTMLImageElement>} The image, or a promise that will resolve to an image.
  732. */
  733. export default TextureAtlas;