@stowkit/three-loader 0.1.25 → 0.1.27

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.
@@ -212,20 +212,25 @@ class MeshParser {
212
212
  * Create Three.js BufferGeometry from Draco compressed mesh data
213
213
  */
214
214
  static async createGeometry(geoInfo, dataBlob, dracoLoader) {
215
+ const startTime = performance.now();
215
216
  // Extract the Draco compressed buffer
216
217
  if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
217
218
  throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
218
219
  }
219
220
  const compressedData = dataBlob.slice(geoInfo.compressedBufferOffset, geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize);
221
+ const extractTime = performance.now();
222
+ reader.PerfLogger.log(`[Perf] Draco extract: ${(extractTime - startTime).toFixed(2)}ms (${geoInfo.compressedBufferSize} bytes)`);
220
223
  // Decode using Draco
221
224
  // Create a blob URL for the Draco data (DRACOLoader expects a URL)
222
225
  const arrayBuffer = compressedData.buffer.slice(compressedData.byteOffset, compressedData.byteOffset + compressedData.byteLength);
223
226
  const blob = new Blob([arrayBuffer]);
224
227
  const url = URL.createObjectURL(blob);
228
+ const decodeStart = performance.now();
225
229
  const geometry = await new Promise((resolve, reject) => {
226
230
  dracoLoader.load(url, (decoded) => {
227
231
  URL.revokeObjectURL(url);
228
- // UVs are pre-flipped in the packer now
232
+ const decodeEnd = performance.now();
233
+ reader.PerfLogger.log(`[Perf] Draco decode: ${(decodeEnd - decodeStart).toFixed(2)}ms (vertices: ${geoInfo.vertexCount}, indices: ${geoInfo.indexCount})`);
229
234
  resolve(decoded);
230
235
  }, undefined, (error) => {
231
236
  URL.revokeObjectURL(url);
@@ -233,8 +238,13 @@ class MeshParser {
233
238
  });
234
239
  });
235
240
  // Compute bounding volumes
241
+ const boundsStart = performance.now();
236
242
  geometry.computeBoundingSphere();
237
243
  geometry.computeBoundingBox();
244
+ const boundsEnd = performance.now();
245
+ reader.PerfLogger.log(`[Perf] Bounds computation: ${(boundsEnd - boundsStart).toFixed(2)}ms`);
246
+ const totalTime = performance.now();
247
+ reader.PerfLogger.log(`[Perf] Total geometry creation: ${(totalTime - startTime).toFixed(2)}ms`);
238
248
  return geometry;
239
249
  }
240
250
  /**
@@ -331,6 +341,7 @@ class MeshParser {
331
341
  */
332
342
  class StowKitPack {
333
343
  constructor(reader, ktx2Loader, dracoLoader) {
344
+ this.textureCache = new Map();
334
345
  this.reader = reader;
335
346
  this.ktx2Loader = ktx2Loader;
336
347
  this.dracoLoader = dracoLoader;
@@ -660,14 +671,19 @@ class StowKitPack {
660
671
  * Load a mesh by its canonical path/name
661
672
  */
662
673
  async loadMesh(assetPath) {
674
+ const totalStart = performance.now();
663
675
  // Find asset by path
676
+ const findStart = performance.now();
664
677
  const assetIndex = this.reader.findAssetByPath(assetPath);
665
678
  if (assetIndex < 0) {
666
679
  throw new Error(`Mesh not found: ${assetPath}`);
667
680
  }
681
+ reader.PerfLogger.log(`[Perf] Find asset: ${(performance.now() - findStart).toFixed(2)}ms`);
668
682
  // Read mesh data and metadata
683
+ const readStart = performance.now();
669
684
  const data = this.reader.readAssetData(assetIndex);
670
685
  const metadata = this.reader.readAssetMetadata(assetIndex);
686
+ reader.PerfLogger.log(`[Perf] Read mesh data: ${(performance.now() - readStart).toFixed(2)}ms (data: ${data?.byteLength || 0} bytes, metadata: ${metadata?.byteLength || 0} bytes)`);
671
687
  if (!data) {
672
688
  throw new Error(`Failed to read mesh data: ${assetPath}`);
673
689
  }
@@ -675,29 +691,46 @@ class StowKitPack {
675
691
  throw new Error(`No metadata available: ${assetPath}`);
676
692
  }
677
693
  // Parse mesh data
694
+ const parseStart = performance.now();
678
695
  const parsedData = MeshParser.parseMeshData(metadata, data);
696
+ reader.PerfLogger.log(`[Perf] Parse mesh metadata: ${(performance.now() - parseStart).toFixed(2)}ms`);
679
697
  // Load textures for materials
698
+ const textureStart = performance.now();
680
699
  await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
700
+ reader.PerfLogger.log(`[Perf] Load textures: ${(performance.now() - textureStart).toFixed(2)}ms`);
681
701
  // Build Three.js scene with Draco decoder
702
+ const buildStart = performance.now();
682
703
  const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
704
+ reader.PerfLogger.log(`[Perf] Build scene: ${(performance.now() - buildStart).toFixed(2)}ms`);
705
+ reader.PerfLogger.log(`[Perf] ===== Total mesh load: ${(performance.now() - totalStart).toFixed(2)}ms =====`);
683
706
  return scene;
684
707
  }
685
708
  /**
686
709
  * Load a texture by its canonical path/name
687
710
  */
688
711
  async loadTexture(assetPath) {
689
- // Find asset by path
690
- const assetIndex = this.reader.findAssetByPath(assetPath);
691
- if (assetIndex < 0) {
692
- throw new Error(`Texture not found: ${assetPath}`);
693
- }
694
- // Read texture data
695
- const data = this.reader.readAssetData(assetIndex);
696
- if (!data) {
697
- throw new Error(`Failed to read texture data: ${assetPath}`);
712
+ // Check cache first
713
+ if (this.textureCache.has(assetPath)) {
714
+ reader.PerfLogger.log(`[Perf] Texture cache hit: ${assetPath}`);
715
+ return this.textureCache.get(assetPath);
698
716
  }
699
- // Load as KTX2
700
- return await this.loadKTX2Texture(data);
717
+ // Cache the promise to avoid duplicate loads
718
+ const loadPromise = (async () => {
719
+ // Find asset by path
720
+ const assetIndex = this.reader.findAssetByPath(assetPath);
721
+ if (assetIndex < 0) {
722
+ throw new Error(`Texture not found: ${assetPath}`);
723
+ }
724
+ // Read texture data
725
+ const data = this.reader.readAssetData(assetIndex);
726
+ if (!data) {
727
+ throw new Error(`Failed to read texture data: ${assetPath}`);
728
+ }
729
+ // Load as KTX2
730
+ return await this.loadKTX2Texture(data);
731
+ })();
732
+ this.textureCache.set(assetPath, loadPromise);
733
+ return loadPromise;
701
734
  }
702
735
  /**
703
736
  * Load audio by its canonical path/name
@@ -976,36 +1009,54 @@ class StowKitPack {
976
1009
  * Close the pack and free resources
977
1010
  */
978
1011
  dispose() {
1012
+ this.textureCache.clear();
979
1013
  this.reader.close();
980
1014
  }
981
1015
  // Private methods moved from StowKitLoader
982
1016
  async loadMaterialTextures(materialData, materials) {
1017
+ // Collect all texture load promises to load in parallel
1018
+ const textureLoads = [];
983
1019
  for (let i = 0; i < materialData.length; i++) {
984
1020
  const matData = materialData[i];
985
- const material = materials[i];
986
1021
  for (const prop of matData.properties) {
987
- // Trim and check for truly non-empty texture ID
988
1022
  const textureId = prop.textureId?.trim();
989
1023
  if (textureId && textureId.length > 0) {
990
- try {
991
- const texture = await this.loadTexture(textureId);
992
- this.applyTextureToMaterial(material, prop.fieldName, texture);
993
- }
994
- catch (error) {
995
- console.error(`Failed to load texture "${textureId}":`, error);
996
- }
1024
+ textureLoads.push({
1025
+ materialIndex: i,
1026
+ propertyName: prop.fieldName,
1027
+ texturePromise: this.loadTexture(textureId).catch(error => {
1028
+ console.error(`Failed to load texture "${textureId}":`, error);
1029
+ throw error;
1030
+ })
1031
+ });
997
1032
  }
998
1033
  }
999
1034
  }
1035
+ // Load all textures in parallel
1036
+ const results = await Promise.allSettled(textureLoads.map(load => load.texturePromise));
1037
+ // Apply successfully loaded textures
1038
+ for (let i = 0; i < textureLoads.length; i++) {
1039
+ const result = results[i];
1040
+ if (result.status === 'fulfilled') {
1041
+ const load = textureLoads[i];
1042
+ const material = materials[load.materialIndex];
1043
+ this.applyTextureToMaterial(material, load.propertyName, result.value);
1044
+ }
1045
+ }
1000
1046
  }
1001
1047
  async loadKTX2Texture(data) {
1048
+ performance.now();
1002
1049
  const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
1003
1050
  const blob = new Blob([arrayBuffer]);
1004
1051
  const url = URL.createObjectURL(blob);
1052
+ reader.PerfLogger.log(`[Perf] KTX2/Basis texture size: ${data.byteLength} bytes`);
1005
1053
  try {
1006
1054
  return await new Promise((resolve, reject) => {
1055
+ const loadStart = performance.now();
1007
1056
  this.ktx2Loader.load(url, (texture) => {
1008
1057
  URL.revokeObjectURL(url);
1058
+ const loadEnd = performance.now();
1059
+ reader.PerfLogger.log(`[Perf] Basis transcode: ${(loadEnd - loadStart).toFixed(2)}ms (resolution: ${texture.image?.width || '?'}x${texture.image?.height || '?'})`);
1009
1060
  texture.needsUpdate = true;
1010
1061
  resolve(texture);
1011
1062
  }, undefined, (error) => {
@@ -1138,6 +1189,10 @@ Object.defineProperty(exports, 'AssetType', {
1138
1189
  enumerable: true,
1139
1190
  get: function () { return reader.AssetType; }
1140
1191
  });
1192
+ Object.defineProperty(exports, 'PerfLogger', {
1193
+ enumerable: true,
1194
+ get: function () { return reader.PerfLogger; }
1195
+ });
1141
1196
  exports.StowKitLoader = StowKitLoader;
1142
1197
  exports.StowKitPack = StowKitPack;
1143
1198
  //# sourceMappingURL=stowkit-three-loader.js.map