minerender 2.1.0-alpha.6 → 2.1.0-alpha.8

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.
@@ -534,14 +534,13 @@ declare class MinecraftFaceUV {
534
534
  }
535
535
 
536
536
  /**
537
- * Releases every long-lived timer MineRender holds, so a Node process can exit.
537
+ * Stops the shared Ticker and request queues, and clears the in-memory caches.
538
538
  *
539
- * Both `@inventivetalent/loading-cache` (cache expiry) and `jobqu` (request queue draining) use
540
- * self-rescheduling timers that are not unref'd, which keeps the event loop alive for as long as
541
- * the library is loaded. Browsers don't care, but anything headless - a render server, a CLI, a
542
- * test run - has to call this when it's done.
539
+ * Idle Node processes can exit without calling this: cache and Ticker timers are unref'd,
540
+ * and idle request queues hold no timer. Pending requests keep their normal timer references.
543
541
  *
544
- * The library stays usable afterwards; the caches and queues simply start over.
542
+ * Call this only when finished with MineRender. Waiting and future requests reject; requests
543
+ * already running can finish. Queue shutdown is permanent, and cache expiry timers do not restart.
545
544
  */
546
545
  declare function shutdown(): void;
547
546
 
@@ -853,6 +852,7 @@ declare class EntityObject extends SceneObject {
853
852
  constructor(entity: EntityModel, options?: Partial<EntityObjectOptions>);
854
853
  init(): Promise<void>;
855
854
  dispose(): void;
855
+ private get textureKey();
856
856
  protected loadTextures(): Promise<void>;
857
857
  protected createMeshes(force?: boolean): void;
858
858
  protected applyTextures(): Promise<void>;
@@ -1219,8 +1219,8 @@ declare class Caching {
1219
1219
  /**
1220
1220
  * Clears every cache and stops its expiry timer.
1221
1221
  *
1222
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
1223
- * this is called - see {@link shutdown}.
1222
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
1223
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
1224
1224
  */
1225
1225
  static end(): void;
1226
1226
  static get cacheSizes(): {};
@@ -1473,8 +1473,9 @@ declare class Requests {
1473
1473
  mcAsset: number;
1474
1474
  };
1475
1475
  /**
1476
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
1477
- * exit until this is called - see {@link shutdown}.
1476
+ * Permanently stops both queues and rejects waiting and future requests.
1477
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
1478
+ * this cleanup. See {@link shutdown}.
1478
1479
  */
1479
1480
  static end(): void;
1480
1481
  }
@@ -1511,6 +1512,9 @@ declare class Skins {
1511
1512
  }
1512
1513
 
1513
1514
  declare class TextureLoader {
1515
+ private static readonly failedTextures;
1516
+ /** Reports whether image loading failed, so cached textures can be retried. */
1517
+ static hasFailed(texture: Texture): boolean;
1514
1518
  protected static createTexture(): Texture;
1515
1519
  static loadInBackground(src: string, format?: PixelFormat, rotation?: number): Texture;
1516
1520
  static load(src: string, format?: PixelFormat, rotation?: number): Texture;
@@ -1620,7 +1624,7 @@ declare class BrowserCache extends PersistentCache<LocalForage> {
1620
1624
  *
1621
1625
  * `image-size` pulls in `fs`/`path` at module scope, so it can only live in the Node build.
1622
1626
  * Everything MineRender loads is a PNG in practice; GIF and JPEG are handled as a courtesy and
1623
- * anything else falls through to an empty result (callers treat that as 0x0).
1627
+ * anything else falls through to an empty result, which ImageLoader rejects.
1624
1628
  */
1625
1629
  declare function probeImageSize(data: Uint8Array): ImageSizeInfo;
1626
1630
 
@@ -534,14 +534,13 @@ declare class MinecraftFaceUV {
534
534
  }
535
535
 
536
536
  /**
537
- * Releases every long-lived timer MineRender holds, so a Node process can exit.
537
+ * Stops the shared Ticker and request queues, and clears the in-memory caches.
538
538
  *
539
- * Both `@inventivetalent/loading-cache` (cache expiry) and `jobqu` (request queue draining) use
540
- * self-rescheduling timers that are not unref'd, which keeps the event loop alive for as long as
541
- * the library is loaded. Browsers don't care, but anything headless - a render server, a CLI, a
542
- * test run - has to call this when it's done.
539
+ * Idle Node processes can exit without calling this: cache and Ticker timers are unref'd,
540
+ * and idle request queues hold no timer. Pending requests keep their normal timer references.
543
541
  *
544
- * The library stays usable afterwards; the caches and queues simply start over.
542
+ * Call this only when finished with MineRender. Waiting and future requests reject; requests
543
+ * already running can finish. Queue shutdown is permanent, and cache expiry timers do not restart.
545
544
  */
546
545
  declare function shutdown(): void;
547
546
 
@@ -853,6 +852,7 @@ declare class EntityObject extends SceneObject {
853
852
  constructor(entity: EntityModel, options?: Partial<EntityObjectOptions>);
854
853
  init(): Promise<void>;
855
854
  dispose(): void;
855
+ private get textureKey();
856
856
  protected loadTextures(): Promise<void>;
857
857
  protected createMeshes(force?: boolean): void;
858
858
  protected applyTextures(): Promise<void>;
@@ -1219,8 +1219,8 @@ declare class Caching {
1219
1219
  /**
1220
1220
  * Clears every cache and stops its expiry timer.
1221
1221
  *
1222
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
1223
- * this is called - see {@link shutdown}.
1222
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
1223
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
1224
1224
  */
1225
1225
  static end(): void;
1226
1226
  static get cacheSizes(): {};
@@ -1473,8 +1473,9 @@ declare class Requests {
1473
1473
  mcAsset: number;
1474
1474
  };
1475
1475
  /**
1476
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
1477
- * exit until this is called - see {@link shutdown}.
1476
+ * Permanently stops both queues and rejects waiting and future requests.
1477
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
1478
+ * this cleanup. See {@link shutdown}.
1478
1479
  */
1479
1480
  static end(): void;
1480
1481
  }
@@ -1511,6 +1512,9 @@ declare class Skins {
1511
1512
  }
1512
1513
 
1513
1514
  declare class TextureLoader {
1515
+ private static readonly failedTextures;
1516
+ /** Reports whether image loading failed, so cached textures can be retried. */
1517
+ static hasFailed(texture: Texture): boolean;
1514
1518
  protected static createTexture(): Texture;
1515
1519
  static loadInBackground(src: string, format?: PixelFormat, rotation?: number): Texture;
1516
1520
  static load(src: string, format?: PixelFormat, rotation?: number): Texture;
@@ -1620,7 +1624,7 @@ declare class BrowserCache extends PersistentCache<LocalForage> {
1620
1624
  *
1621
1625
  * `image-size` pulls in `fs`/`path` at module scope, so it can only live in the Node build.
1622
1626
  * Everything MineRender loads is a PNG in practice; GIF and JPEG are handled as a courtesy and
1623
- * anything else falls through to an empty result (callers treat that as 0x0).
1627
+ * anything else falls through to an empty result, which ImageLoader rejects.
1624
1628
  */
1625
1629
  declare function probeImageSize(data: Uint8Array): ImageSizeInfo;
1626
1630
 
@@ -576,8 +576,8 @@ var Caching = class {
576
576
  /**
577
577
  * Clears every cache and stops its expiry timer.
578
578
  *
579
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
580
- * this is called - see {@link shutdown}.
579
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
580
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
581
581
  */
582
582
  static end() {
583
583
  for (const cache of this.all) {
@@ -825,13 +825,14 @@ var _Requests = class _Requests {
825
825
  }
826
826
  static get queueSizes() {
827
827
  return {
828
- generic: this.genericQueue.size,
829
- mcAsset: this.mcAssetRequestQueue.size
828
+ generic: this.genericQueue.size + this.genericQueue.activeSize,
829
+ mcAsset: this.mcAssetRequestQueue.size + this.mcAssetRequestQueue.activeSize
830
830
  };
831
831
  }
832
832
  /**
833
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
834
- * exit until this is called - see {@link shutdown}.
833
+ * Permanently stops both queues and rejects waiting and future requests.
834
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
835
+ * this cleanup. See {@link shutdown}.
835
836
  */
836
837
  static end() {
837
838
  this.genericQueue.end();
@@ -911,29 +912,41 @@ var ImageLoader = class _ImageLoader {
911
912
  static async loadAsync(src) {
912
913
  return new Promise((resolve, reject) => {
913
914
  const image = this._createImage();
914
- image.src = src;
915
915
  image.onload = () => resolve(image);
916
- image.onerror = (err) => reject(err);
916
+ image.onerror = (err) => reject(err instanceof Error ? err : new Error("Failed to decode image"));
917
+ image.src = src;
917
918
  });
918
919
  }
919
920
  static loadElement(src, onload, onerr) {
920
921
  const image = this._createImage();
921
- image.src = src;
922
922
  if (onload)
923
923
  image.onload = onload;
924
924
  if (onerr)
925
925
  image.onerror = onerr;
926
+ image.src = src;
926
927
  return image;
927
928
  }
928
929
  static async loadData(src) {
929
930
  console.debug(p2, "loadData", src);
930
- return await this.infoToData(await this.getInfo(src));
931
+ const image = await this.loadCanvasData(src);
932
+ return image.data.getImageData(0, 0, image.width, image.height);
931
933
  }
932
934
  static async loadCanvasData(src) {
933
- return await this.infoToCanvasData(await this.getInfo(src));
935
+ const keyStr = serializeImageKey({ src });
936
+ const info = this.getInfo(src);
937
+ const cached = Caching.rawImageCache.getIfPresent(keyStr);
938
+ try {
939
+ return await this.infoToCanvasData(await info);
940
+ } catch (err) {
941
+ if (cached && Caching.rawImageCache.getIfPresent(keyStr) === cached) {
942
+ Caching.rawImageCache.invalidate(keyStr);
943
+ }
944
+ throw err;
945
+ }
934
946
  }
935
947
  static async infoToCanvasData(info) {
936
- const image = await _ImageLoader.loadAsync(info.src);
948
+ const type = info.type === "jpg" ? "jpeg" : info.type ?? "png";
949
+ const image = await _ImageLoader.loadAsync(`data:image/${type};base64,${info.data.toString("base64")}`);
937
950
  const canvas = createCanvas(info.width, info.height);
938
951
  const context = canvas.getContext("2d");
939
952
  context.drawImage(image, 0, 0);
@@ -944,11 +957,8 @@ var ImageLoader = class _ImageLoader {
944
957
  };
945
958
  }
946
959
  static async infoToData(info) {
947
- const image = await _ImageLoader.loadAsync(info.src);
948
- const canvas = createCanvas(info.width, info.height);
949
- const context = canvas.getContext("2d");
950
- context.drawImage(image, 0, 0);
951
- return context.getImageData(0, 0, canvas.width, canvas.height);
960
+ const image = await this.infoToCanvasData(info);
961
+ return image.data.getImageData(0, 0, image.width, image.height);
952
962
  }
953
963
  static async getData(src) {
954
964
  const keyStr = serializeImageKey({ src });
@@ -963,13 +973,16 @@ var ImageLoader = class _ImageLoader {
963
973
  });
964
974
  }
965
975
  static async processResponse(response) {
966
- const src = response.config.url;
976
+ const src = response.config?.url;
967
977
  const data = import_buffer.Buffer.from(response.data);
968
978
  const { width, height, type } = Env.provider.imageSize(data);
979
+ if (!width || !height) {
980
+ throw new Error("Invalid or unsupported image dimensions");
981
+ }
969
982
  return {
970
983
  src,
971
- width: width || 0,
972
- height: height || 0,
984
+ width,
985
+ height,
973
986
  type,
974
987
  data
975
988
  };
@@ -986,15 +999,7 @@ var ImageLoader = class _ImageLoader {
986
999
  return Requests.genericRequest({
987
1000
  url: src,
988
1001
  responseType: "arraybuffer"
989
- }).then(this.processResponse).catch((err) => {
990
- return {
991
- src,
992
- width: 0,
993
- height: 0,
994
- type: void 0,
995
- data: import_buffer.Buffer.from([0])
996
- };
997
- });
1002
+ }).then(this.processResponse);
998
1003
  }
999
1004
  static async getInfo(src) {
1000
1005
  const keyStr = serializeImageKey({ src });
@@ -1016,16 +1021,22 @@ var ImageLoader = class _ImageLoader {
1016
1021
 
1017
1022
  // src/texture/TextureLoader.ts
1018
1023
  var TextureLoader = class {
1024
+ /** Reports whether image loading failed, so cached textures can be retried. */
1025
+ static hasFailed(texture) {
1026
+ return this.failedTextures.has(texture);
1027
+ }
1019
1028
  static createTexture() {
1020
1029
  return new import_three4.Texture();
1021
1030
  }
1022
1031
  static loadInBackground(src, format = import_three4.RGBAFormat, rotation = 0) {
1023
1032
  const texture = this.createTexture();
1024
1033
  texture.colorSpace = THREE.SRGBColorSpace;
1025
- const image = ImageLoader.loadElement(src);
1026
- image.onload = function() {
1034
+ const image = ImageLoader.loadElement(src, () => {
1027
1035
  texture.needsUpdate = true;
1028
- };
1036
+ }, (error) => {
1037
+ this.failedTextures.add(texture);
1038
+ console.warn("Failed to load texture", src, error);
1039
+ });
1029
1040
  texture.image = image;
1030
1041
  texture.format = format;
1031
1042
  texture.rotation = rotation;
@@ -1035,14 +1046,18 @@ var TextureLoader = class {
1035
1046
  const texture = new import_three4.Texture();
1036
1047
  texture.colorSpace = THREE.SRGBColorSpace;
1037
1048
  ImageLoader.getData(src).then((image) => {
1038
- texture.needsUpdate = true;
1039
1049
  texture.image = image;
1050
+ texture.needsUpdate = true;
1051
+ }).catch((error) => {
1052
+ this.failedTextures.add(texture);
1053
+ console.warn("Failed to load texture", src, error);
1040
1054
  });
1041
1055
  texture.format = format;
1042
1056
  texture.rotation = rotation;
1043
1057
  return Textures.initTextureProps(texture);
1044
1058
  }
1045
1059
  };
1060
+ TextureLoader.failedTextures = /* @__PURE__ */ new WeakSet();
1046
1061
 
1047
1062
  // src/texture/Textures.ts
1048
1063
  var THREE2 = __toESM(require("three"));
@@ -1066,6 +1081,10 @@ var Textures = class _Textures {
1066
1081
  }
1067
1082
  static getImage(key) {
1068
1083
  const keyStr = serializeTextureKey(key);
1084
+ const cached = Caching.textureCache.peek(keyStr);
1085
+ if (cached && TextureLoader.hasFailed(cached)) {
1086
+ Caching.textureCache.invalidate(keyStr);
1087
+ }
1069
1088
  return Caching.textureCache.get(keyStr, (k) => {
1070
1089
  return _Textures.createImage(key);
1071
1090
  });
@@ -1210,6 +1229,10 @@ var _Materials = class _Materials {
1210
1229
  }
1211
1230
  static getImage(key) {
1212
1231
  const keyStr = serializeMaterialKey(key);
1232
+ const map = Caching.materialCache.peek(keyStr)?.map;
1233
+ if (map && TextureLoader.hasFailed(map)) {
1234
+ Caching.materialCache.invalidate(keyStr);
1235
+ }
1213
1236
  return Caching.materialCache.get(keyStr, (k) => {
1214
1237
  return _Materials.createImage(key);
1215
1238
  });
@@ -1867,11 +1890,21 @@ var ModelTextures = class {
1867
1890
  return this._persistentMetaCache ?? (this._persistentMetaCache = PersistentCache.open("minerender-texturemeta"));
1868
1891
  }
1869
1892
  static async get(key) {
1870
- const asset = await this.preload(key);
1871
- if (asset) {
1872
- return ImageLoader.infoToCanvasData(asset);
1893
+ const keyStr = key.serialize();
1894
+ const pending = this.preload(key);
1895
+ const cached = Caching.textureAssetCache.getIfPresent(keyStr);
1896
+ try {
1897
+ const asset = await pending;
1898
+ if (asset) {
1899
+ return await ImageLoader.infoToCanvasData(asset);
1900
+ }
1901
+ return void 0;
1902
+ } catch (err) {
1903
+ if (cached && Caching.textureAssetCache.getIfPresent(keyStr) === cached) {
1904
+ Caching.textureAssetCache.invalidate(keyStr);
1905
+ }
1906
+ throw err;
1873
1907
  }
1874
- return void 0;
1875
1908
  }
1876
1909
  static async preload(key) {
1877
1910
  const keyStr = key.serialize();
@@ -3062,8 +3095,8 @@ var EntityObject = class extends SceneObject {
3062
3095
  dispose() {
3063
3096
  super.dispose();
3064
3097
  }
3065
- async loadTextures() {
3066
- this.imageData = await ModelTextures.get(new AssetKey(
3098
+ get textureKey() {
3099
+ return new AssetKey(
3067
3100
  this.entity.key?.namespace ?? DEFAULT_NAMESPACE,
3068
3101
  this.entity.key.path,
3069
3102
  //TODO: texture may differ from entity name; most of them are in subdirectories for multiple variants etc.
@@ -3071,7 +3104,10 @@ var EntityObject = class extends SceneObject {
3071
3104
  "entity",
3072
3105
  "assets",
3073
3106
  ".png"
3074
- ));
3107
+ );
3108
+ }
3109
+ async loadTextures() {
3110
+ this.imageData = await ModelTextures.get(this.textureKey);
3075
3111
  }
3076
3112
  //TODO: abstract this, since extracted model data can be used for entities, blocks, players
3077
3113
  createMeshes(force = false) {
@@ -3104,20 +3140,23 @@ var EntityObject = class extends SceneObject {
3104
3140
  this.meshesCreated = true;
3105
3141
  }
3106
3142
  async applyTextures() {
3107
- const textureAsset = await ModelTextures.preload(new AssetKey(
3108
- this.entity.key?.namespace ?? DEFAULT_NAMESPACE,
3109
- this.entity.key.path,
3110
- "textures",
3111
- "entity",
3112
- "assets",
3113
- ".png"
3114
- ));
3115
- if (textureAsset) {
3116
- let mat = Materials.getImage({ texture: { src: textureAsset.src } });
3117
- this.iterateAllMeshes((mesh) => {
3118
- mesh.material = mat;
3119
- });
3120
- }
3143
+ const assetKeyStr = this.textureKey.serialize();
3144
+ const keyStr = `entity:${assetKeyStr}`;
3145
+ let mat = Caching.materialCache.getIfPresent(keyStr);
3146
+ if (!mat) {
3147
+ const pending = this.loadTextures();
3148
+ const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
3149
+ await pending;
3150
+ if (!this.imageData) return;
3151
+ const canvas = this.imageData.data.canvas;
3152
+ const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
3153
+ mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
3154
+ this.imageData = void 0;
3155
+ }
3156
+ this.iterateAllMeshes((mesh) => {
3157
+ mesh.material = mat;
3158
+ });
3159
+ this.notifyDirty();
3121
3160
  }
3122
3161
  };
3123
3162
  EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, {});