@bloopjs/toodle 0.0.102 → 0.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,14 +8,13 @@ import { FontPipeline } from "../text/FontPipeline";
8
8
  import { MsdfFont } from "../text/MsdfFont";
9
9
  import { TextShader } from "../text/TextShader";
10
10
  import { assert } from "../utils/mod";
11
+ import { Bundles } from "./Bundles";
11
12
  import { TextureComputeShader } from "./TextureComputeShader";
12
13
  import type {
13
14
  AtlasBundleOpts,
14
15
  AtlasCoords,
15
16
  CpuTextureAtlas,
16
- PixiRegion,
17
17
  TextureBundleOpts,
18
- TextureRegion,
19
18
  TextureWithMetadata,
20
19
  } from "./types";
21
20
  import { getBitmapFromUrl, packBitmapsToAtlas } from "./util";
@@ -24,18 +23,18 @@ export type TextureId = string;
24
23
  export type BundleId = string;
25
24
  export type FontId = string;
26
25
 
27
- type Bundle = {
28
- atlases: CpuTextureAtlas[];
29
- isLoaded: boolean;
30
- atlasIndices: number[];
26
+ export type AssetManagerOptions = {
27
+ /** Existing Bundles instance to use for CPU-side storage. If not provided, a new one is created. */
28
+ bundles?: Bundles;
29
+ /** Texture format (default: "rgba8unorm") */
30
+ format?: "rgba8unorm" | "rg8unorm";
31
31
  };
32
32
 
33
33
  export class AssetManager {
34
34
  readonly textureAtlas: GPUTexture;
35
+ readonly bundles: Bundles;
35
36
  #device: GPUDevice;
36
37
  #presentationFormat: GPUTextureFormat;
37
- #bundles: Map<BundleId, Bundle> = new Map();
38
- #textures: Map<string, AtlasCoords[]> = new Map();
39
38
  #fonts: Map<string, TextShader> = new Map();
40
39
  #cropComputeShader: TextureComputeShader;
41
40
  #limits: Limits;
@@ -45,11 +44,14 @@ export class AssetManager {
45
44
  device: GPUDevice,
46
45
  presentationFormat: GPUTextureFormat,
47
46
  limits: Limits,
48
- format: "rgba8unorm" | "rg8unorm" = "rgba8unorm",
47
+ options: AssetManagerOptions = {},
49
48
  ) {
50
49
  this.#device = device;
51
50
  this.#presentationFormat = presentationFormat;
52
51
  this.#limits = limits;
52
+ this.bundles =
53
+ options.bundles ?? new Bundles({ atlasSize: limits.textureSize });
54
+ const format = options.format ?? "rgba8unorm";
53
55
  this.textureAtlas = device.createTexture({
54
56
  label: "Asset Manager Atlas Texture",
55
57
  size: [
@@ -108,27 +110,27 @@ export class AssetManager {
108
110
  * @returns Whether the image has been cropped (i.e. if it has uvScaledCropped)
109
111
  */
110
112
  isCropped(id: TextureId): boolean {
111
- if (!this.#textures.has(id)) {
113
+ if (!this.bundles.hasTexture(id)) {
112
114
  throw new Error(
113
115
  `Texture ${id} not found in atlas. Have you called toodle.loadTextures with this id or toodle.loadBundle with a bundle that contains it?`,
114
116
  );
115
117
  }
116
118
 
117
- return this.#textures.get(id)![0].uvScaleCropped === undefined;
119
+ return this.bundles.getAtlasCoords(id)[0].uvScaleCropped === undefined;
118
120
  }
119
121
 
120
122
  /**
121
123
  * A read-only map of all currently loaded textures.
122
124
  */
123
125
  get textures() {
124
- return this.#textures;
126
+ return this.bundles.textures;
125
127
  }
126
128
 
127
129
  /**
128
130
  * A read-only array of all currently loaded texture ids.
129
131
  */
130
132
  get textureIds() {
131
- return Array.from(this.#textures.keys());
133
+ return this.bundles.textureIds;
132
134
  }
133
135
 
134
136
  /**
@@ -200,7 +202,7 @@ export class AssetManager {
200
202
  atlasIndex,
201
203
  };
202
204
 
203
- this.#textures.set(id, [coords]);
205
+ this.bundles.addTextureEntry(id, coords);
204
206
  this.#availableIndices.delete(atlasIndex);
205
207
 
206
208
  textureWrapper.texture.destroy();
@@ -226,7 +228,8 @@ export class AssetManager {
226
228
  await this.#registerBundleFromAtlases(bundleId, opts);
227
229
  }
228
230
 
229
- if (opts.autoLoad) {
231
+ const autoLoad = opts.autoLoad ?? true;
232
+ if (autoLoad) {
230
233
  await this.loadBundle(bundleId);
231
234
  }
232
235
  return bundleId;
@@ -238,22 +241,25 @@ export class AssetManager {
238
241
  * See: https://toodle.gg/f849595b3ed13fc956fc1459a5cb5f0228f9d259/examples/texture-bundles.html
239
242
  */
240
243
  async loadBundle(bundleId: BundleId) {
241
- const bundle = this.#bundles.get(bundleId);
242
- if (!bundle) {
244
+ if (!this.bundles.hasBundle(bundleId)) {
243
245
  throw new Error(`Bundle ${bundleId} not found`);
244
246
  }
245
247
 
246
- if (bundle.isLoaded) {
248
+ if (this.bundles.isBundleLoaded(bundleId)) {
247
249
  console.warn(`Bundle ${bundleId} is already loaded.`);
248
250
  return;
249
251
  }
250
252
 
251
- for (const atlas of bundle.atlases) {
253
+ const atlases = this.bundles.getBundleAtlases(bundleId);
254
+ const atlasIndices: number[] = [];
255
+
256
+ for (const atlas of atlases) {
252
257
  const atlasIndex = await this.extra.loadAtlas(atlas);
253
- bundle.atlasIndices.push(atlasIndex);
258
+ atlasIndices.push(atlasIndex);
254
259
  }
255
260
 
256
- bundle.isLoaded = true;
261
+ // Use setBundleLoaded (not markBundleLoaded) since loadAtlas already populated textures
262
+ this.bundles.setBundleLoaded(bundleId, atlasIndices);
257
263
  }
258
264
 
259
265
  /**
@@ -263,24 +269,21 @@ export class AssetManager {
263
269
  * @param bundleId - The id of the bundle to unload
264
270
  */
265
271
  async unloadBundle(bundleId: BundleId) {
266
- const bundle = this.#bundles.get(bundleId);
267
- if (!bundle) {
272
+ if (!this.bundles.hasBundle(bundleId)) {
268
273
  throw new Error(`Bundle ${bundleId} not found`);
269
274
  }
270
275
 
271
- if (!bundle.isLoaded) {
276
+ if (!this.bundles.isBundleLoaded(bundleId)) {
272
277
  console.warn(`Bundle ${bundleId} is not loaded.`);
273
278
  return;
274
279
  }
275
280
 
281
+ const atlasIndices = this.bundles.getBundleAtlasIndices(bundleId);
276
282
  await Promise.all(
277
- bundle.atlasIndices.map((atlasIndex) =>
278
- this.extra.unloadAtlas(atlasIndex),
279
- ),
283
+ atlasIndices.map((atlasIndex) => this.extra.unloadAtlas(atlasIndex)),
280
284
  );
281
285
 
282
- bundle.isLoaded = false;
283
- bundle.atlasIndices = [];
286
+ this.bundles.unloadBundle(bundleId);
284
287
  }
285
288
 
286
289
  /**
@@ -328,15 +331,13 @@ export class AssetManager {
328
331
  )
329
332
  return;
330
333
 
331
- const coords: AtlasCoords[] | undefined = this.#textures.get(
332
- node.textureId,
333
- );
334
- if (!coords || !coords.length) {
334
+ if (!this.bundles.hasTexture(node.textureId)) {
335
335
  throw new Error(
336
336
  `Node ${node.id} references an invalid texture ${node.textureId}.`,
337
337
  );
338
338
  }
339
339
 
340
+ const coords = this.bundles.getAtlasCoords(node.textureId);
340
341
  if (
341
342
  coords.find((coord) => coord.atlasIndex === node.atlasCoords.atlasIndex)
342
343
  )
@@ -345,22 +346,6 @@ export class AssetManager {
345
346
  node.extra.setAtlasCoords(coords[0]);
346
347
  }
347
348
 
348
- /**
349
- * Sets a designated texture ID to the corresponding `AtlasRegion` built from a `TextureRegion` and `numerical atlas index.
350
- * @param id - `String` representing the texture name. I.e. "PlayerSprite"
351
- * @param textureRegion - `TextureRegion` corresponding the uv and texture offsets
352
- * @param atlasIndex - `number` of the atlas that the texture will live in.
353
- * @private
354
- */
355
- #addTexture(id: string, textureRegion: TextureRegion, atlasIndex: number) {
356
- this.#textures.set(id, [
357
- {
358
- ...textureRegion,
359
- atlasIndex,
360
- },
361
- ]);
362
- }
363
-
364
349
  /**
365
350
  *
366
351
  * @param bitmap - `ImageBitmap` to be processed into a `GPUTexture` for storage and manipulation
@@ -422,115 +407,12 @@ export class AssetManager {
422
407
  this.#device,
423
408
  );
424
409
 
425
- this.#bundles.set(bundleId, {
426
- atlases,
427
- atlasIndices: [],
428
- isLoaded: false,
429
- });
410
+ this.bundles.registerDynamicBundle(bundleId, atlases);
430
411
  }
431
412
 
432
413
  async #registerBundleFromAtlases(bundleId: BundleId, opts: AtlasBundleOpts) {
433
- const atlases: CpuTextureAtlas[] = [];
434
-
435
- for (const atlas of opts.atlases) {
436
- const jsonUrl =
437
- atlas.json ??
438
- new URL(
439
- atlas.png!.toString().replace(".png", ".json"),
440
- atlas.png!.origin,
441
- );
442
- const pngUrl =
443
- atlas.png ??
444
- new URL(
445
- atlas.json!.toString().replace(".json", ".png"),
446
- atlas.json!.origin,
447
- );
448
-
449
- const atlasDef = await (await fetch(jsonUrl)).json();
450
- const bitmap = !opts.rg8
451
- ? await getBitmapFromUrl(pngUrl)
452
- : await createImageBitmap(new ImageData(1, 1)); // placeholder bitmap if using rg8
453
-
454
- let rg8Bytes: Uint8Array<ArrayBuffer> | undefined;
455
- if (opts.rg8) {
456
- const rg8url = new URL(
457
- pngUrl.toString().replace(".png", ".rg8.gz"),
458
- pngUrl.origin,
459
- );
460
- const rgBytes = await fetch(rg8url).then(async (r) => {
461
- const enc = (r.headers.get("content-encoding") || "").toLowerCase();
462
- // If server/CDN already set Content-Encoding, Fetch returns decompressed bytes.
463
- if (
464
- enc.includes("gzip") ||
465
- enc.includes("br") ||
466
- enc.includes("deflate")
467
- ) {
468
- return new Uint8Array(await r.arrayBuffer());
469
- }
470
-
471
- assert(r.body, "Response body of rg8 file is null");
472
- const ds = new DecompressionStream("gzip");
473
- const ab = await new Response(r.body.pipeThrough(ds)).arrayBuffer();
474
- return new Uint8Array(ab);
475
- });
476
- rg8Bytes = rgBytes;
477
- }
478
-
479
- const cpuTextureAtlas: CpuTextureAtlas = {
480
- texture: bitmap,
481
- rg8Bytes,
482
- textureRegions: new Map(),
483
- width: opts.rg8 ? this.#limits.textureSize : bitmap.width,
484
- height: opts.rg8 ? this.#limits.textureSize : bitmap.height,
485
- };
486
-
487
- for (const [assetId, frame] of Object.entries(atlasDef.frames) as [
488
- string,
489
- PixiRegion,
490
- ][]) {
491
- const leftCrop = frame.spriteSourceSize.x;
492
- const rightCrop =
493
- frame.sourceSize.w -
494
- frame.spriteSourceSize.x -
495
- frame.spriteSourceSize.w;
496
- const topCrop = frame.spriteSourceSize.y;
497
- const bottomCrop =
498
- frame.sourceSize.h -
499
- frame.spriteSourceSize.y -
500
- frame.spriteSourceSize.h;
501
-
502
- cpuTextureAtlas.textureRegions.set(assetId, {
503
- cropOffset: {
504
- x: leftCrop - rightCrop,
505
- y: bottomCrop - topCrop,
506
- },
507
- originalSize: {
508
- width: frame.sourceSize.w,
509
- height: frame.sourceSize.h,
510
- },
511
- uvOffset: {
512
- x: frame.frame.x / cpuTextureAtlas.width,
513
- y: frame.frame.y / cpuTextureAtlas.height,
514
- },
515
- uvScale: {
516
- width: frame.sourceSize.w / cpuTextureAtlas.width,
517
- height: frame.sourceSize.h / cpuTextureAtlas.height,
518
- },
519
- uvScaleCropped: {
520
- width: frame.frame.w / cpuTextureAtlas.width,
521
- height: frame.frame.h / cpuTextureAtlas.height,
522
- },
523
- });
524
- }
525
-
526
- atlases.push(cpuTextureAtlas);
527
- }
528
-
529
- this.#bundles.set(bundleId, {
530
- atlases,
531
- atlasIndices: [],
532
- isLoaded: false,
533
- });
414
+ // Delegate to the Bundles instance for atlas parsing
415
+ await this.bundles.registerAtlasBundle(bundleId, opts);
534
416
  }
535
417
 
536
418
  /**
@@ -539,14 +421,12 @@ export class AssetManager {
539
421
  extra = {
540
422
  // Get an array of all currently registered bundle ids.
541
423
  getRegisteredBundleIds: (): string[] => {
542
- return this.#bundles ? Array.from(this.#bundles.keys()) : [];
424
+ return this.bundles.getRegisteredBundleIds();
543
425
  },
544
426
 
545
427
  // Get an array of all currently loaded bundle ids.
546
428
  getLoadedBundleIds: (): string[] => {
547
- return Array.from(this.#bundles.entries())
548
- .filter(([, value]) => value.isLoaded)
549
- .map(([key]) => key);
429
+ return this.bundles.getLoadedBundleIds();
550
430
  },
551
431
 
552
432
  /**
@@ -558,14 +438,7 @@ export class AssetManager {
558
438
  * @param coords - The atlas coordinates to set
559
439
  */
560
440
  setAtlasCoords: (id: TextureId, coords: AtlasCoords) => {
561
- const oldCoords: AtlasCoords[] | undefined = this.#textures.get(id);
562
- if (!oldCoords) return;
563
- const indexToModify = oldCoords.findIndex(
564
- (coord) => coord.atlasIndex === coords.atlasIndex,
565
- );
566
- if (indexToModify === -1) return;
567
- oldCoords[indexToModify] = coords;
568
- this.#textures.set(id, oldCoords);
441
+ this.bundles.setAtlasCoords(id, coords);
569
442
  },
570
443
 
571
444
  /**
@@ -575,12 +448,7 @@ export class AssetManager {
575
448
  * @returns An array of the atlas coordinates for the texture
576
449
  */
577
450
  getAtlasCoords: (id: TextureId): AtlasCoords[] => {
578
- if (!this.#textures.has(id)) {
579
- throw new Error(
580
- `Texture ${id} not found in atlas. Have you called toodle.loadBundle with a bundle that contains this id (or toodle.loadTextures with this id as a key)?`,
581
- );
582
- }
583
- return this.#textures.get(id) ?? [];
451
+ return this.bundles.getAtlasCoords(id);
584
452
  },
585
453
 
586
454
  /**
@@ -590,13 +458,7 @@ export class AssetManager {
590
458
  * @returns Point of the texture's associated X,Y offset
591
459
  */
592
460
  getTextureOffset: (id: TextureId): Vec2 => {
593
- const texture: AtlasCoords[] | undefined = this.#textures.get(id);
594
- if (!texture) {
595
- throw new Error(
596
- `Texture ${id} not found in atlas. Have you called toodle.loadTextures with this id or toodle.loadBundle with a bundle that contains it?`,
597
- );
598
- }
599
- return texture[0].cropOffset;
461
+ return this.bundles.getTextureOffset(id);
600
462
  },
601
463
 
602
464
  /**
@@ -675,10 +537,7 @@ export class AssetManager {
675
537
  }
676
538
 
677
539
  for (const [id, region] of atlas.textureRegions) {
678
- const existing = this.#textures.get(id);
679
- if (existing) {
680
- existing.push({ ...region, atlasIndex });
681
- } else this.#addTexture(id, region, atlasIndex);
540
+ this.bundles.addTextureEntry(id, { ...region, atlasIndex });
682
541
  }
683
542
  return atlasIndex;
684
543
  },
@@ -690,17 +549,7 @@ export class AssetManager {
690
549
  */
691
550
  unloadAtlas: async (atlasIndex: number) => {
692
551
  this.#availableIndices.add(atlasIndex);
693
- for (const [id, coords] of this.#textures.entries()) {
694
- const indexToModify = coords.findIndex(
695
- (coord) => coord.atlasIndex === atlasIndex,
696
- );
697
- if (indexToModify !== -1) {
698
- coords.splice(indexToModify, 1);
699
- }
700
- if (!coords.length) {
701
- this.#textures.delete(id);
702
- }
703
- }
552
+ this.bundles.removeTextureEntriesForAtlas(atlasIndex);
704
553
  },
705
554
  };
706
555