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.
@@ -535,14 +535,13 @@ declare class MinecraftFaceUV {
535
535
  }
536
536
 
537
537
  /**
538
- * Releases every long-lived timer MineRender holds, so a Node process can exit.
538
+ * Stops the shared Ticker and request queues, and clears the in-memory caches.
539
539
  *
540
- * Both `@inventivetalent/loading-cache` (cache expiry) and `jobqu` (request queue draining) use
541
- * self-rescheduling timers that are not unref'd, which keeps the event loop alive for as long as
542
- * the library is loaded. Browsers don't care, but anything headless - a render server, a CLI, a
543
- * test run - has to call this when it's done.
540
+ * Idle Node processes can exit without calling this: cache and Ticker timers are unref'd,
541
+ * and idle request queues hold no timer. Pending requests keep their normal timer references.
544
542
  *
545
- * The library stays usable afterwards; the caches and queues simply start over.
543
+ * Call this only when finished with MineRender. Waiting and future requests reject; requests
544
+ * already running can finish. Queue shutdown is permanent, and cache expiry timers do not restart.
546
545
  */
547
546
  declare function shutdown(): void;
548
547
 
@@ -854,6 +853,7 @@ declare class EntityObject extends SceneObject {
854
853
  constructor(entity: EntityModel, options?: Partial<EntityObjectOptions>);
855
854
  init(): Promise<void>;
856
855
  dispose(): void;
856
+ private get textureKey();
857
857
  protected loadTextures(): Promise<void>;
858
858
  protected createMeshes(force?: boolean): void;
859
859
  protected applyTextures(): Promise<void>;
@@ -1220,8 +1220,8 @@ declare class Caching {
1220
1220
  /**
1221
1221
  * Clears every cache and stops its expiry timer.
1222
1222
  *
1223
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
1224
- * this is called - see {@link shutdown}.
1223
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
1224
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
1225
1225
  */
1226
1226
  static end(): void;
1227
1227
  static get cacheSizes(): {};
@@ -1474,8 +1474,9 @@ declare class Requests {
1474
1474
  mcAsset: number;
1475
1475
  };
1476
1476
  /**
1477
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
1478
- * exit until this is called - see {@link shutdown}.
1477
+ * Permanently stops both queues and rejects waiting and future requests.
1478
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
1479
+ * this cleanup. See {@link shutdown}.
1479
1480
  */
1480
1481
  static end(): void;
1481
1482
  }
@@ -1512,6 +1513,9 @@ declare class Skins {
1512
1513
  }
1513
1514
 
1514
1515
  declare class TextureLoader {
1516
+ private static readonly failedTextures;
1517
+ /** Reports whether image loading failed, so cached textures can be retried. */
1518
+ static hasFailed(texture: Texture): boolean;
1515
1519
  protected static createTexture(): Texture;
1516
1520
  static loadInBackground(src: string, format?: PixelFormat, rotation?: number): Texture;
1517
1521
  static load(src: string, format?: PixelFormat, rotation?: number): Texture;
@@ -535,14 +535,13 @@ declare class MinecraftFaceUV {
535
535
  }
536
536
 
537
537
  /**
538
- * Releases every long-lived timer MineRender holds, so a Node process can exit.
538
+ * Stops the shared Ticker and request queues, and clears the in-memory caches.
539
539
  *
540
- * Both `@inventivetalent/loading-cache` (cache expiry) and `jobqu` (request queue draining) use
541
- * self-rescheduling timers that are not unref'd, which keeps the event loop alive for as long as
542
- * the library is loaded. Browsers don't care, but anything headless - a render server, a CLI, a
543
- * test run - has to call this when it's done.
540
+ * Idle Node processes can exit without calling this: cache and Ticker timers are unref'd,
541
+ * and idle request queues hold no timer. Pending requests keep their normal timer references.
544
542
  *
545
- * The library stays usable afterwards; the caches and queues simply start over.
543
+ * Call this only when finished with MineRender. Waiting and future requests reject; requests
544
+ * already running can finish. Queue shutdown is permanent, and cache expiry timers do not restart.
546
545
  */
547
546
  declare function shutdown(): void;
548
547
 
@@ -854,6 +853,7 @@ declare class EntityObject extends SceneObject {
854
853
  constructor(entity: EntityModel, options?: Partial<EntityObjectOptions>);
855
854
  init(): Promise<void>;
856
855
  dispose(): void;
856
+ private get textureKey();
857
857
  protected loadTextures(): Promise<void>;
858
858
  protected createMeshes(force?: boolean): void;
859
859
  protected applyTextures(): Promise<void>;
@@ -1220,8 +1220,8 @@ declare class Caching {
1220
1220
  /**
1221
1221
  * Clears every cache and stops its expiry timer.
1222
1222
  *
1223
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
1224
- * this is called - see {@link shutdown}.
1223
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
1224
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
1225
1225
  */
1226
1226
  static end(): void;
1227
1227
  static get cacheSizes(): {};
@@ -1474,8 +1474,9 @@ declare class Requests {
1474
1474
  mcAsset: number;
1475
1475
  };
1476
1476
  /**
1477
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
1478
- * exit until this is called - see {@link shutdown}.
1477
+ * Permanently stops both queues and rejects waiting and future requests.
1478
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
1479
+ * this cleanup. See {@link shutdown}.
1479
1480
  */
1480
1481
  static end(): void;
1481
1482
  }
@@ -1512,6 +1513,9 @@ declare class Skins {
1512
1513
  }
1513
1514
 
1514
1515
  declare class TextureLoader {
1516
+ private static readonly failedTextures;
1517
+ /** Reports whether image loading failed, so cached textures can be retried. */
1518
+ static hasFailed(texture: Texture): boolean;
1515
1519
  protected static createTexture(): Texture;
1516
1520
  static loadInBackground(src: string, format?: PixelFormat, rotation?: number): Texture;
1517
1521
  static load(src: string, format?: PixelFormat, rotation?: number): Texture;
@@ -538,8 +538,8 @@ var Caching = class {
538
538
  /**
539
539
  * Clears every cache and stops its expiry timer.
540
540
  *
541
- * loading-cache reschedules that timer indefinitely, so a Node process will not exit until
542
- * this is called - see {@link shutdown}.
541
+ * Expiry timers are unref'd in Node, so idle processes can exit without this cleanup.
542
+ * Use {@link shutdown} to also stop requests and the shared Ticker.
543
543
  */
544
544
  static end() {
545
545
  for (const cache of this.all) {
@@ -787,13 +787,14 @@ var _Requests = class _Requests {
787
787
  }
788
788
  static get queueSizes() {
789
789
  return {
790
- generic: this.genericQueue.size,
791
- mcAsset: this.mcAssetRequestQueue.size
790
+ generic: this.genericQueue.size + this.genericQueue.activeSize,
791
+ mcAsset: this.mcAssetRequestQueue.size + this.mcAssetRequestQueue.activeSize
792
792
  };
793
793
  }
794
794
  /**
795
- * Stops both queue timers. jobqu reschedules itself indefinitely, so a Node process will not
796
- * exit until this is called - see {@link shutdown}.
795
+ * Permanently stops both queues and rejects waiting and future requests.
796
+ * Requests already running can finish. Idle queues hold no timer, so Node can exit without
797
+ * this cleanup. See {@link shutdown}.
797
798
  */
798
799
  static end() {
799
800
  this.genericQueue.end();
@@ -873,29 +874,41 @@ var ImageLoader = class _ImageLoader {
873
874
  static async loadAsync(src) {
874
875
  return new Promise((resolve, reject) => {
875
876
  const image = this._createImage();
876
- image.src = src;
877
877
  image.onload = () => resolve(image);
878
- image.onerror = (err) => reject(err);
878
+ image.onerror = (err) => reject(err instanceof Error ? err : new Error("Failed to decode image"));
879
+ image.src = src;
879
880
  });
880
881
  }
881
882
  static loadElement(src, onload, onerr) {
882
883
  const image = this._createImage();
883
- image.src = src;
884
884
  if (onload)
885
885
  image.onload = onload;
886
886
  if (onerr)
887
887
  image.onerror = onerr;
888
+ image.src = src;
888
889
  return image;
889
890
  }
890
891
  static async loadData(src) {
891
892
  console.debug(p2, "loadData", src);
892
- return await this.infoToData(await this.getInfo(src));
893
+ const image = await this.loadCanvasData(src);
894
+ return image.data.getImageData(0, 0, image.width, image.height);
893
895
  }
894
896
  static async loadCanvasData(src) {
895
- return await this.infoToCanvasData(await this.getInfo(src));
897
+ const keyStr = serializeImageKey({ src });
898
+ const info = this.getInfo(src);
899
+ const cached = Caching.rawImageCache.getIfPresent(keyStr);
900
+ try {
901
+ return await this.infoToCanvasData(await info);
902
+ } catch (err) {
903
+ if (cached && Caching.rawImageCache.getIfPresent(keyStr) === cached) {
904
+ Caching.rawImageCache.invalidate(keyStr);
905
+ }
906
+ throw err;
907
+ }
896
908
  }
897
909
  static async infoToCanvasData(info) {
898
- const image = await _ImageLoader.loadAsync(info.src);
910
+ const type = info.type === "jpg" ? "jpeg" : info.type ?? "png";
911
+ const image = await _ImageLoader.loadAsync(`data:image/${type};base64,${info.data.toString("base64")}`);
899
912
  const canvas = createCanvas2(info.width, info.height);
900
913
  const context = canvas.getContext("2d");
901
914
  context.drawImage(image, 0, 0);
@@ -906,11 +919,8 @@ var ImageLoader = class _ImageLoader {
906
919
  };
907
920
  }
908
921
  static async infoToData(info) {
909
- const image = await _ImageLoader.loadAsync(info.src);
910
- const canvas = createCanvas2(info.width, info.height);
911
- const context = canvas.getContext("2d");
912
- context.drawImage(image, 0, 0);
913
- return context.getImageData(0, 0, canvas.width, canvas.height);
922
+ const image = await this.infoToCanvasData(info);
923
+ return image.data.getImageData(0, 0, image.width, image.height);
914
924
  }
915
925
  static async getData(src) {
916
926
  const keyStr = serializeImageKey({ src });
@@ -925,13 +935,17 @@ var ImageLoader = class _ImageLoader {
925
935
  });
926
936
  }
927
937
  static async processResponse(response) {
928
- const src = response.config.url;
938
+ var _a;
939
+ const src = (_a = response.config) == null ? void 0 : _a.url;
929
940
  const data = import_buffer.Buffer.from(response.data);
930
941
  const { width, height, type } = Env.provider.imageSize(data);
942
+ if (!width || !height) {
943
+ throw new Error("Invalid or unsupported image dimensions");
944
+ }
931
945
  return {
932
946
  src,
933
- width: width || 0,
934
- height: height || 0,
947
+ width,
948
+ height,
935
949
  type,
936
950
  data
937
951
  };
@@ -948,15 +962,7 @@ var ImageLoader = class _ImageLoader {
948
962
  return Requests.genericRequest({
949
963
  url: src,
950
964
  responseType: "arraybuffer"
951
- }).then(this.processResponse).catch((err) => {
952
- return {
953
- src,
954
- width: 0,
955
- height: 0,
956
- type: void 0,
957
- data: import_buffer.Buffer.from([0])
958
- };
959
- });
965
+ }).then(this.processResponse);
960
966
  }
961
967
  static async getInfo(src) {
962
968
  const keyStr = serializeImageKey({ src });
@@ -978,16 +984,22 @@ var ImageLoader = class _ImageLoader {
978
984
 
979
985
  // src/texture/TextureLoader.ts
980
986
  var TextureLoader = class {
987
+ /** Reports whether image loading failed, so cached textures can be retried. */
988
+ static hasFailed(texture) {
989
+ return this.failedTextures.has(texture);
990
+ }
981
991
  static createTexture() {
982
992
  return new import_three4.Texture();
983
993
  }
984
994
  static loadInBackground(src, format = import_three4.RGBAFormat, rotation = 0) {
985
995
  const texture = this.createTexture();
986
996
  texture.colorSpace = THREE.SRGBColorSpace;
987
- const image = ImageLoader.loadElement(src);
988
- image.onload = function() {
997
+ const image = ImageLoader.loadElement(src, () => {
989
998
  texture.needsUpdate = true;
990
- };
999
+ }, (error) => {
1000
+ this.failedTextures.add(texture);
1001
+ console.warn("Failed to load texture", src, error);
1002
+ });
991
1003
  texture.image = image;
992
1004
  texture.format = format;
993
1005
  texture.rotation = rotation;
@@ -997,14 +1009,18 @@ var TextureLoader = class {
997
1009
  const texture = new import_three4.Texture();
998
1010
  texture.colorSpace = THREE.SRGBColorSpace;
999
1011
  ImageLoader.getData(src).then((image) => {
1000
- texture.needsUpdate = true;
1001
1012
  texture.image = image;
1013
+ texture.needsUpdate = true;
1014
+ }).catch((error) => {
1015
+ this.failedTextures.add(texture);
1016
+ console.warn("Failed to load texture", src, error);
1002
1017
  });
1003
1018
  texture.format = format;
1004
1019
  texture.rotation = rotation;
1005
1020
  return Textures.initTextureProps(texture);
1006
1021
  }
1007
1022
  };
1023
+ TextureLoader.failedTextures = /* @__PURE__ */ new WeakSet();
1008
1024
 
1009
1025
  // src/texture/Textures.ts
1010
1026
  var THREE2 = __toESM(require("three"));
@@ -1028,6 +1044,10 @@ var Textures = class _Textures {
1028
1044
  }
1029
1045
  static getImage(key) {
1030
1046
  const keyStr = serializeTextureKey(key);
1047
+ const cached = Caching.textureCache.peek(keyStr);
1048
+ if (cached && TextureLoader.hasFailed(cached)) {
1049
+ Caching.textureCache.invalidate(keyStr);
1050
+ }
1031
1051
  return Caching.textureCache.get(keyStr, (k) => {
1032
1052
  return _Textures.createImage(key);
1033
1053
  });
@@ -1171,7 +1191,12 @@ var _Materials = class _Materials {
1171
1191
  return this.createBasicCanvasMaterial(canvas, transparent, shade);
1172
1192
  }
1173
1193
  static getImage(key) {
1194
+ var _a;
1174
1195
  const keyStr = serializeMaterialKey(key);
1196
+ const map = (_a = Caching.materialCache.peek(keyStr)) == null ? void 0 : _a.map;
1197
+ if (map && TextureLoader.hasFailed(map)) {
1198
+ Caching.materialCache.invalidate(keyStr);
1199
+ }
1175
1200
  return Caching.materialCache.get(keyStr, (k) => {
1176
1201
  return _Materials.createImage(key);
1177
1202
  });
@@ -1829,11 +1854,21 @@ var ModelTextures = class {
1829
1854
  return this._persistentMetaCache ??= PersistentCache.open("minerender-texturemeta");
1830
1855
  }
1831
1856
  static async get(key) {
1832
- const asset = await this.preload(key);
1833
- if (asset) {
1834
- return ImageLoader.infoToCanvasData(asset);
1857
+ const keyStr = key.serialize();
1858
+ const pending = this.preload(key);
1859
+ const cached = Caching.textureAssetCache.getIfPresent(keyStr);
1860
+ try {
1861
+ const asset = await pending;
1862
+ if (asset) {
1863
+ return await ImageLoader.infoToCanvasData(asset);
1864
+ }
1865
+ return void 0;
1866
+ } catch (err) {
1867
+ if (cached && Caching.textureAssetCache.getIfPresent(keyStr) === cached) {
1868
+ Caching.textureAssetCache.invalidate(keyStr);
1869
+ }
1870
+ throw err;
1835
1871
  }
1836
- return void 0;
1837
1872
  }
1838
1873
  static async preload(key) {
1839
1874
  const keyStr = key.serialize();
@@ -3027,9 +3062,9 @@ var EntityObject = class extends SceneObject {
3027
3062
  dispose() {
3028
3063
  super.dispose();
3029
3064
  }
3030
- async loadTextures() {
3065
+ get textureKey() {
3031
3066
  var _a;
3032
- this.imageData = await ModelTextures.get(new AssetKey(
3067
+ return new AssetKey(
3033
3068
  ((_a = this.entity.key) == null ? void 0 : _a.namespace) ?? DEFAULT_NAMESPACE,
3034
3069
  this.entity.key.path,
3035
3070
  //TODO: texture may differ from entity name; most of them are in subdirectories for multiple variants etc.
@@ -3037,7 +3072,10 @@ var EntityObject = class extends SceneObject {
3037
3072
  "entity",
3038
3073
  "assets",
3039
3074
  ".png"
3040
- ));
3075
+ );
3076
+ }
3077
+ async loadTextures() {
3078
+ this.imageData = await ModelTextures.get(this.textureKey);
3041
3079
  }
3042
3080
  //TODO: abstract this, since extracted model data can be used for entities, blocks, players
3043
3081
  createMeshes(force = false) {
@@ -3070,21 +3108,23 @@ var EntityObject = class extends SceneObject {
3070
3108
  this.meshesCreated = true;
3071
3109
  }
3072
3110
  async applyTextures() {
3073
- var _a;
3074
- const textureAsset = await ModelTextures.preload(new AssetKey(
3075
- ((_a = this.entity.key) == null ? void 0 : _a.namespace) ?? DEFAULT_NAMESPACE,
3076
- this.entity.key.path,
3077
- "textures",
3078
- "entity",
3079
- "assets",
3080
- ".png"
3081
- ));
3082
- if (textureAsset) {
3083
- let mat = Materials.getImage({ texture: { src: textureAsset.src } });
3084
- this.iterateAllMeshes((mesh) => {
3085
- mesh.material = mat;
3086
- });
3087
- }
3111
+ const assetKeyStr = this.textureKey.serialize();
3112
+ const keyStr = `entity:${assetKeyStr}`;
3113
+ let mat = Caching.materialCache.getIfPresent(keyStr);
3114
+ if (!mat) {
3115
+ const pending = this.loadTextures();
3116
+ const cachedAsset = Caching.textureAssetCache.getIfPresent(assetKeyStr);
3117
+ await pending;
3118
+ if (!this.imageData) return;
3119
+ const canvas = this.imageData.data.canvas;
3120
+ const createMaterial = () => Materials.createBasicCanvasMaterial(canvas);
3121
+ mat = cachedAsset && Caching.textureAssetCache.getIfPresent(assetKeyStr) === cachedAsset ? Caching.materialCache.get(keyStr, createMaterial) : createMaterial();
3122
+ this.imageData = void 0;
3123
+ }
3124
+ this.iterateAllMeshes((mesh) => {
3125
+ mesh.material = mat;
3126
+ });
3127
+ this.notifyDirty();
3088
3128
  }
3089
3129
  };
3090
3130
  EntityObject.DEFAULT_OPTIONS = (0, import_ts_deepmerge4.default)({}, SceneObject.DEFAULT_OPTIONS, {});