@stowkit/three-loader 0.1.2 → 0.1.4

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  var THREE = require('three');
4
4
  var KTX2Loader_js = require('three/examples/jsm/loaders/KTX2Loader.js');
5
+ var DRACOLoader_js = require('three/examples/jsm/loaders/DRACOLoader.js');
5
6
  var reader = require('@stowkit/reader');
6
7
 
7
8
  function _interopNamespaceDefault(e) {
@@ -51,8 +52,8 @@ class MeshParser {
51
52
  console.log(`Mesh counts: ${metadata.meshGeometryCount} geometries, ${metadata.materialCount} materials, ${metadata.nodeCount} nodes`);
52
53
  // Check if the metadata blob contains just the header or all the mesh info
53
54
  const expectedMetadataSize = 140 +
54
- (metadata.meshGeometryCount * 76) +
55
- (metadata.materialCount * 64) +
55
+ (metadata.meshGeometryCount * 40) + // Draco compressed (40 bytes each)
56
+ (metadata.materialCount * 196) + // MaterialData header
56
57
  (metadata.nodeCount * 116);
57
58
  let hasExtendedMetadata = metadataBlob.length >= expectedMetadataSize;
58
59
  console.log(`Expected metadata size: ${expectedMetadataSize}, actual: ${metadataBlob.length}, has extended: ${hasExtendedMetadata}`);
@@ -60,28 +61,25 @@ class MeshParser {
60
61
  const sourceBlob = hasExtendedMetadata ? metadataBlob : dataBlob;
61
62
  const metaView = new DataView(sourceBlob.buffer, sourceBlob.byteOffset, sourceBlob.byteLength);
62
63
  let offset = hasExtendedMetadata ? 140 : 0; // Start after MeshMetadata if in metadata blob, or at beginning of data blob
63
- // Parse geometry infos
64
+ // Parse geometry infos (now Draco compressed!)
64
65
  const geometries = [];
65
66
  for (let i = 0; i < metadata.meshGeometryCount; i++) {
66
- if (offset + 76 > sourceBlob.length) {
67
+ if (offset + 40 > sourceBlob.length) {
67
68
  console.warn(`Data truncated at geometry ${i}/${metadata.meshGeometryCount}`);
68
69
  break;
69
70
  }
70
71
  const geo = {
71
72
  vertexCount: metaView.getUint32(offset, true),
72
73
  indexCount: metaView.getUint32(offset + 4, true),
73
- vertexBufferOffset: Number(metaView.getBigUint64(offset + 8, true)),
74
- indexBufferOffset: Number(metaView.getBigUint64(offset + 16, true)),
75
- normalBufferOffset: Number(metaView.getBigUint64(offset + 24, true)),
76
- uvBufferOffset: Number(metaView.getBigUint64(offset + 32, true)),
77
- vertexBufferSize: Number(metaView.getBigUint64(offset + 40, true)),
78
- indexBufferSize: Number(metaView.getBigUint64(offset + 48, true)),
79
- normalBufferSize: Number(metaView.getBigUint64(offset + 56, true)),
80
- uvBufferSize: Number(metaView.getBigUint64(offset + 64, true)),
81
- materialIndex: metaView.getUint32(offset + 72, true)
74
+ hasNormals: metaView.getUint32(offset + 8, true),
75
+ hasUVs: metaView.getUint32(offset + 12, true),
76
+ compressedBufferOffset: Number(metaView.getBigUint64(offset + 16, true)),
77
+ compressedBufferSize: Number(metaView.getBigUint64(offset + 24, true)),
78
+ materialIndex: metaView.getUint32(offset + 32, true),
79
+ padding: metaView.getUint32(offset + 36, true)
82
80
  };
83
81
  geometries.push(geo);
84
- offset += 76; // Size of MeshGeometryInfo
82
+ offset += 40; // Size of MeshGeometryInfo (Draco compressed)
85
83
  }
86
84
  // Parse materials - MaterialData structure: name[64] + schema_id[128] + property_count(4) = 196 bytes
87
85
  const materialData = [];
@@ -208,76 +206,40 @@ class MeshParser {
208
206
  return { metadata, geometries, materials, materialData, nodes, meshIndices };
209
207
  }
210
208
  /**
211
- * Create Three.js BufferGeometry from mesh data
209
+ * Create Three.js BufferGeometry from Draco compressed mesh data
212
210
  */
213
- static createGeometry(geoInfo, dataBlob) {
214
- console.log('Creating geometry:', {
211
+ static async createGeometry(geoInfo, dataBlob, dracoLoader) {
212
+ console.log('Decoding Draco geometry:', {
215
213
  vertexCount: geoInfo.vertexCount,
216
214
  indexCount: geoInfo.indexCount,
217
- vertexOffset: geoInfo.vertexBufferOffset,
218
- vertexSize: geoInfo.vertexBufferSize,
219
- dataBlobSize: dataBlob.length
215
+ hasNormals: geoInfo.hasNormals,
216
+ hasUVs: geoInfo.hasUVs,
217
+ compressedOffset: geoInfo.compressedBufferOffset,
218
+ compressedSize: geoInfo.compressedBufferSize
220
219
  });
221
- const geometry = new THREE__namespace.BufferGeometry();
222
- const dataView = new DataView(dataBlob.buffer, dataBlob.byteOffset, dataBlob.byteLength);
223
- // Extract vertex positions (3 floats per vertex)
224
- if (geoInfo.vertexBufferSize > 0 && geoInfo.vertexBufferOffset + geoInfo.vertexBufferSize <= dataBlob.length) {
225
- const positions = new Float32Array(geoInfo.vertexCount * 3);
226
- for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
227
- positions[i] = dataView.getFloat32(geoInfo.vertexBufferOffset + i * 4, true);
228
- }
229
- geometry.setAttribute('position', new THREE__namespace.BufferAttribute(positions, 3));
230
- console.log('Added positions:', positions.slice(0, 9)); // First 3 vertices
231
- }
232
- else {
233
- console.warn('No vertex data or out of bounds');
234
- }
235
- // Extract normals
236
- if (geoInfo.normalBufferSize > 0 && geoInfo.normalBufferOffset + geoInfo.normalBufferSize <= dataBlob.length) {
237
- const normals = new Float32Array(geoInfo.vertexCount * 3);
238
- for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
239
- normals[i] = dataView.getFloat32(geoInfo.normalBufferOffset + i * 4, true);
240
- }
241
- geometry.setAttribute('normal', new THREE__namespace.BufferAttribute(normals, 3));
242
- }
243
- else {
244
- // Compute normals if not provided
245
- geometry.computeVertexNormals();
246
- }
247
- // Extract UVs
248
- if (geoInfo.uvBufferSize > 0 && geoInfo.uvBufferOffset + geoInfo.uvBufferSize <= dataBlob.length) {
249
- const uvs = new Float32Array(geoInfo.vertexCount * 2);
250
- for (let i = 0; i < geoInfo.vertexCount * 2; i += 2) {
251
- uvs[i] = dataView.getFloat32(geoInfo.uvBufferOffset + i * 4, true); // U
252
- uvs[i + 1] = 1.0 - dataView.getFloat32(geoInfo.uvBufferOffset + (i + 1) * 4, true); // V (flipped)
253
- }
254
- geometry.setAttribute('uv', new THREE__namespace.BufferAttribute(uvs, 2));
255
- }
256
- // Extract indices
257
- if (geoInfo.indexCount > 0 && geoInfo.indexBufferOffset + geoInfo.indexBufferSize <= dataBlob.length) {
258
- const indices = new Uint32Array(geoInfo.indexCount);
259
- // Read the first index to determine the base offset
260
- let minIndex = Number.MAX_SAFE_INTEGER;
261
- for (let i = 0; i < geoInfo.indexCount; i++) {
262
- const index = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
263
- if (index < minIndex)
264
- minIndex = index;
265
- }
266
- // Read indices again and offset them to start from 0
267
- for (let i = 0; i < geoInfo.indexCount; i++) {
268
- const absoluteIndex = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
269
- indices[i] = absoluteIndex - minIndex;
270
- }
271
- geometry.setIndex(new THREE__namespace.BufferAttribute(indices, 1));
272
- console.log(`Added indices (offset by ${minIndex}):`, indices.slice(0, 12)); // First 4 triangles
273
- }
274
- else if (geoInfo.indexCount > 0) {
275
- console.warn('Index data out of bounds');
276
- }
277
- // Ensure we have normals (compute if missing)
278
- if (!geometry.attributes.normal) {
279
- geometry.computeVertexNormals();
220
+ // Extract the Draco compressed buffer
221
+ if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
222
+ throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
280
223
  }
224
+ const compressedData = dataBlob.slice(geoInfo.compressedBufferOffset, geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize);
225
+ // Decode using Draco
226
+ // Create a blob URL for the Draco data (DRACOLoader expects a URL)
227
+ const arrayBuffer = compressedData.buffer.slice(compressedData.byteOffset, compressedData.byteOffset + compressedData.byteLength);
228
+ const blob = new Blob([arrayBuffer]);
229
+ const url = URL.createObjectURL(blob);
230
+ const geometry = await new Promise((resolve, reject) => {
231
+ dracoLoader.load(url, (decoded) => {
232
+ URL.revokeObjectURL(url);
233
+ console.log('Draco decoded successfully:', decoded);
234
+ // UVs are pre-flipped in the packer now, no need to flip here
235
+ resolve(decoded);
236
+ }, undefined, (error) => {
237
+ URL.revokeObjectURL(url);
238
+ console.error('Draco decode failed:', error);
239
+ reject(new Error(`Failed to decode Draco geometry: ${error}`));
240
+ });
241
+ });
242
+ // Compute bounding volumes
281
243
  geometry.computeBoundingSphere();
282
244
  geometry.computeBoundingBox();
283
245
  console.log('Final geometry:', {
@@ -292,7 +254,7 @@ class MeshParser {
292
254
  /**
293
255
  * Build Three.js scene from parsed mesh data
294
256
  */
295
- static buildScene(parsedData, dataBlob) {
257
+ static async buildScene(parsedData, dataBlob, dracoLoader) {
296
258
  const root = new THREE__namespace.Group();
297
259
  root.name = 'StowKitMesh';
298
260
  const { geometries, materials, nodes, meshIndices } = parsedData;
@@ -320,7 +282,7 @@ class MeshParser {
320
282
  if (meshIndex < geometries.length) {
321
283
  const geoInfo = geometries[meshIndex];
322
284
  console.log(` Creating geometry from geoInfo: vertices=${geoInfo.vertexCount}, indices=${geoInfo.indexCount}, material=${geoInfo.materialIndex}`);
323
- const geometry = this.createGeometry(geoInfo, dataBlob);
285
+ const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
324
286
  // Use assigned material if valid, otherwise create default
325
287
  let material;
326
288
  if (geoInfo.materialIndex < materials.length) {
@@ -363,9 +325,10 @@ class MeshParser {
363
325
  // If no nodes, create a single mesh from all geometries
364
326
  if (nodes.length === 0 && geometries.length > 0) {
365
327
  console.log('No nodes found, creating meshes directly from geometries');
366
- geometries.forEach((geoInfo, index) => {
328
+ for (let index = 0; index < geometries.length; index++) {
329
+ const geoInfo = geometries[index];
367
330
  console.log(`Creating direct mesh ${index}: materialIndex=${geoInfo.materialIndex}, vertices=${geoInfo.vertexCount}`);
368
- const geometry = this.createGeometry(geoInfo, dataBlob);
331
+ const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
369
332
  // Use assigned material if valid, otherwise create default
370
333
  let material;
371
334
  if (geoInfo.materialIndex < materials.length) {
@@ -384,7 +347,7 @@ class MeshParser {
384
347
  mesh.name = `Mesh_${index}`;
385
348
  root.add(mesh);
386
349
  console.log(`Added mesh ${index} to root`);
387
- });
350
+ }
388
351
  }
389
352
  console.log(`Final scene has ${root.children.length} direct children`);
390
353
  // Debug: traverse and count all meshes, showing full hierarchy
@@ -451,16 +414,26 @@ class StowKitLoader extends THREE__namespace.Loader {
451
414
  this.reader = new reader.StowKitReader(wasmPath);
452
415
  this.ownedReader = true;
453
416
  }
454
- // Setup KTX2 loader for textures with default path
455
- const transcoderPath = parameters?.transcoderPath || '/basis/';
417
+ // Setup KTX2 loader for textures with default path (user must provide basis files)
418
+ const transcoderPath = parameters?.transcoderPath || '/basis/'; // Users still need to provide this
456
419
  this.ktx2Loader = new KTX2Loader_js.KTX2Loader(manager);
457
420
  this.ktx2Loader.setTranscoderPath(transcoderPath);
421
+ // Setup Draco loader for mesh decompression
422
+ this.dracoLoader = new DRACOLoader_js.DRACOLoader(manager);
423
+ this.dracoLoader.setDecoderPath('/stowkit/draco/'); // Draco files copied by postinstall
458
424
  // Detect support with a temporary renderer
459
425
  // This is required for KTX2Loader to work
460
426
  const tempRenderer = new THREE__namespace.WebGLRenderer();
461
427
  this.ktx2Loader.detectSupport(tempRenderer);
462
428
  tempRenderer.dispose();
463
429
  }
430
+ /**
431
+ * Set the path to the Draco decoder
432
+ */
433
+ setDracoDecoderPath(path) {
434
+ this.dracoLoader.setDecoderPath(path);
435
+ return this;
436
+ }
464
437
  /**
465
438
  * Set the path to the Basis Universal transcoder
466
439
  */
@@ -490,6 +463,42 @@ class StowKitLoader extends THREE__namespace.Loader {
490
463
  const arrayBuffer = await response.arrayBuffer();
491
464
  await this.reader.open(arrayBuffer);
492
465
  }
466
+ /**
467
+ * Load and parse a mesh asset by its index
468
+ * @param index - Asset index
469
+ * @param onLoad - Callback when loading completes
470
+ * @param onProgress - Progress callback
471
+ * @param onError - Error callback
472
+ */
473
+ async loadMeshByIndex(index, onLoad, onProgress, onError) {
474
+ try {
475
+ // Read mesh data and metadata
476
+ const data = this.reader.readAssetData(index);
477
+ const metadata = this.reader.readAssetMetadata(index);
478
+ if (!data) {
479
+ throw new Error(`Failed to read mesh data for index ${index}`);
480
+ }
481
+ if (!metadata) {
482
+ throw new Error(`No metadata available for index ${index}`);
483
+ }
484
+ // Parse mesh data
485
+ const parsedData = MeshParser.parseMeshData(metadata, data);
486
+ // Load textures for materials
487
+ await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
488
+ // Build Three.js scene with Draco decoder
489
+ const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
490
+ if (onLoad) {
491
+ onLoad(scene);
492
+ }
493
+ return scene;
494
+ }
495
+ catch (error) {
496
+ if (onError) {
497
+ onError(error);
498
+ }
499
+ throw error;
500
+ }
501
+ }
493
502
  /**
494
503
  * Load and parse a mesh asset from a StowKit pack
495
504
  * @param url - URL to the .stow file (or omit if already opened with openPack)
@@ -522,8 +531,8 @@ class StowKitLoader extends THREE__namespace.Loader {
522
531
  const parsedData = MeshParser.parseMeshData(metadata, data);
523
532
  // Load textures for materials
524
533
  await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
525
- // Build Three.js scene
526
- const scene = MeshParser.buildScene(parsedData, data);
534
+ // Build Three.js scene with Draco decoder
535
+ const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
527
536
  if (onLoad) {
528
537
  onLoad(scene);
529
538
  }
@@ -759,20 +768,20 @@ class StowKitLoader extends THREE__namespace.Loader {
759
768
  if (assetIndex < 0)
760
769
  return null;
761
770
  const metadata = this.reader.readAssetMetadata(assetIndex);
762
- if (!metadata || metadata.length < 148)
763
- return null;
771
+ if (!metadata || metadata.length < 144)
772
+ return null; // TextureMetadata is now 144 bytes
764
773
  const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
765
774
  const decoder = new TextDecoder();
766
- // Parse TextureMetadata structure
767
- const stringIdBytes = metadata.slice(20, 148);
775
+ // Parse TextureMetadata structure (width, height, channels, channel_format, string_id[128])
776
+ const stringIdBytes = metadata.slice(16, 144); // After 4 uint32s (16 bytes)
768
777
  const nullIdx = stringIdBytes.indexOf(0);
769
778
  const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
770
779
  return {
771
780
  width: view.getUint32(0, true),
772
781
  height: view.getUint32(4, true),
773
782
  channels: view.getUint32(8, true),
774
- isKtx2: view.getUint32(12, true) === 1,
775
- channelFormat: view.getUint32(16, true),
783
+ isKtx2: true, // All textures are KTX2 now
784
+ channelFormat: view.getUint32(12, true),
776
785
  stringId
777
786
  };
778
787
  }
@@ -901,8 +910,8 @@ class StowKitLoader extends THREE__namespace.Loader {
901
910
  const materialCount = view.getUint32(4, true);
902
911
  const nodeCount = view.getUint32(8, true);
903
912
  let offset = 140;
904
- // Skip geometries (76 bytes each)
905
- offset += meshGeometryCount * 76;
913
+ // Skip geometries (40 bytes each - Draco compressed)
914
+ offset += meshGeometryCount * 40;
906
915
  // Parse materials
907
916
  const materials = [];
908
917
  for (let i = 0; i < materialCount; i++) {
@@ -1023,6 +1032,7 @@ class StowKitLoader extends THREE__namespace.Loader {
1023
1032
  */
1024
1033
  dispose() {
1025
1034
  this.ktx2Loader.dispose();
1035
+ this.dracoLoader.dispose();
1026
1036
  if (this.ownedReader) {
1027
1037
  this.reader.close();
1028
1038
  }