@stowkit/three-loader 0.1.10 → 0.1.11

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.
@@ -366,167 +366,214 @@ class MeshParser {
366
366
  }
367
367
 
368
368
  /**
369
- * Three.js loader for StowKit asset packs (.stow files)
370
- *
371
- * Usage:
372
- * ```typescript
373
- * const loader = new StowKitLoader();
374
- * loader.setTranscoderPath('/basis/');
375
- *
376
- * // Load a mesh asset
377
- * loader.loadMesh('assets.stow', 'path/to/mesh', (scene) => {
378
- * threeScene.add(scene);
379
- * });
380
- * ```
369
+ * Represents an opened StowKit pack with methods to load assets by name
381
370
  */
382
- class StowKitLoader extends THREE.Loader {
383
- constructor(manager, parameters) {
384
- super(manager);
385
- this.ownedReader = false;
386
- // Use provided reader or create new one
387
- if (parameters?.reader) {
388
- this.reader = parameters.reader;
389
- this.ownedReader = false;
371
+ class StowKitPack {
372
+ constructor(reader, ktx2Loader, dracoLoader) {
373
+ this.reader = reader;
374
+ this.ktx2Loader = ktx2Loader;
375
+ this.dracoLoader = dracoLoader;
376
+ }
377
+ /**
378
+ * Load a mesh by its canonical path/name
379
+ */
380
+ async loadMesh(assetPath) {
381
+ // Find asset by path
382
+ const assetIndex = this.reader.findAssetByPath(assetPath);
383
+ if (assetIndex < 0) {
384
+ throw new Error(`Mesh not found: ${assetPath}`);
385
+ }
386
+ // Read mesh data and metadata
387
+ const data = this.reader.readAssetData(assetIndex);
388
+ const metadata = this.reader.readAssetMetadata(assetIndex);
389
+ if (!data) {
390
+ throw new Error(`Failed to read mesh data: ${assetPath}`);
390
391
  }
391
- else {
392
- const wasmPath = parameters?.wasmPath || '/stowkit_reader.wasm';
393
- this.reader = new StowKitReader(wasmPath);
394
- this.ownedReader = true;
392
+ if (!metadata) {
393
+ throw new Error(`No metadata available: ${assetPath}`);
395
394
  }
396
- // Setup KTX2 loader for textures with default path (bundled with package)
397
- const transcoderPath = parameters?.transcoderPath || '/stowkit/basis/'; // Auto-copied on install
398
- this.ktx2Loader = new KTX2Loader(manager);
399
- this.ktx2Loader.setTranscoderPath(transcoderPath);
400
- // Setup Draco loader for mesh decompression
401
- this.dracoLoader = new DRACOLoader(manager);
402
- this.dracoLoader.setDecoderPath('/stowkit/draco/'); // Draco files copied by postinstall
403
- // Detect support with a temporary renderer
404
- // This is required for KTX2Loader to work
405
- const tempRenderer = new THREE.WebGLRenderer();
406
- this.ktx2Loader.detectSupport(tempRenderer);
407
- tempRenderer.dispose();
395
+ // Parse mesh data
396
+ const parsedData = MeshParser.parseMeshData(metadata, data);
397
+ // Load textures for materials
398
+ await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
399
+ // Build Three.js scene with Draco decoder
400
+ const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
401
+ return scene;
408
402
  }
409
403
  /**
410
- * Set the path to the Draco decoder
404
+ * Load a texture by its canonical path/name
411
405
  */
412
- setDracoDecoderPath(path) {
413
- this.dracoLoader.setDecoderPath(path);
414
- return this;
406
+ async loadTexture(assetPath) {
407
+ // Find asset by path
408
+ const assetIndex = this.reader.findAssetByPath(assetPath);
409
+ if (assetIndex < 0) {
410
+ throw new Error(`Texture not found: ${assetPath}`);
411
+ }
412
+ // Read texture data
413
+ const data = this.reader.readAssetData(assetIndex);
414
+ if (!data) {
415
+ throw new Error(`Failed to read texture data: ${assetPath}`);
416
+ }
417
+ // Load as KTX2
418
+ return await this.loadKTX2Texture(data);
415
419
  }
416
420
  /**
417
- * Set the path to the Basis Universal transcoder
421
+ * Load audio by its canonical path/name
418
422
  */
419
- setTranscoderPath(path) {
420
- this.ktx2Loader.setTranscoderPath(path);
421
- return this;
423
+ async loadAudio(assetPath, listener) {
424
+ // Find asset by path
425
+ const assetIndex = this.reader.findAssetByPath(assetPath);
426
+ if (assetIndex < 0) {
427
+ throw new Error(`Audio not found: ${assetPath}`);
428
+ }
429
+ // Read audio data
430
+ const data = this.reader.readAssetData(assetIndex);
431
+ if (!data) {
432
+ throw new Error(`Failed to read audio data: ${assetPath}`);
433
+ }
434
+ // Create audio object
435
+ const audio = new THREE.Audio(listener);
436
+ // Decode audio data
437
+ const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
438
+ const audioContext = listener.context;
439
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
440
+ audio.setBuffer(audioBuffer);
441
+ return audio;
422
442
  }
423
443
  /**
424
- * Detect WebGL support for compressed textures
444
+ * Get list of all assets in pack
425
445
  */
426
- detectSupport(renderer) {
427
- this.ktx2Loader.detectSupport(renderer);
428
- return this;
446
+ listAssets() {
447
+ return this.reader.listAssets();
429
448
  }
430
449
  /**
431
- * Open a .stow pack file (call this first if loading multiple assets)
432
- * @param url - URL to the .stow file
450
+ * Get asset count
433
451
  */
434
- async openPack(url) {
435
- if (this.ownedReader) {
436
- await this.reader.init();
437
- }
438
- const response = await fetch(url);
439
- if (!response.ok) {
440
- throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
441
- }
442
- const arrayBuffer = await response.arrayBuffer();
443
- await this.reader.open(arrayBuffer);
452
+ getAssetCount() {
453
+ return this.reader.getAssetCount();
444
454
  }
445
455
  /**
446
- * Load and parse a mesh asset by its index
447
- * @param index - Asset index
448
- * @param onLoad - Callback when loading completes
449
- * @param onProgress - Progress callback
450
- * @param onError - Error callback
456
+ * Get asset info by index
451
457
  */
452
- async loadMeshByIndex(index, onLoad, onProgress, onError) {
453
- try {
454
- // Read mesh data and metadata
455
- const data = this.reader.readAssetData(index);
456
- const metadata = this.reader.readAssetMetadata(index);
457
- if (!data) {
458
- throw new Error(`Failed to read mesh data for index ${index}`);
459
- }
460
- if (!metadata) {
461
- throw new Error(`No metadata available for index ${index}`);
462
- }
463
- // Parse mesh data
464
- const parsedData = MeshParser.parseMeshData(metadata, data);
465
- // Load textures for materials
466
- await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
467
- // Build Three.js scene with Draco decoder
468
- const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
469
- if (onLoad) {
470
- onLoad(scene);
471
- }
472
- return scene;
473
- }
474
- catch (error) {
475
- if (onError) {
476
- onError(error);
477
- }
478
- throw error;
479
- }
458
+ getAssetInfo(index) {
459
+ return this.reader.getAssetInfo(index);
480
460
  }
481
461
  /**
482
- * Load and parse a mesh asset from a StowKit pack
483
- * @param url - URL to the .stow file (or omit if already opened with openPack)
484
- * @param assetPath - Path to the mesh asset within the pack
485
- * @param onLoad - Callback when loading completes
486
- * @param onProgress - Progress callback
487
- * @param onError - Error callback
462
+ * Read asset data by index
488
463
  */
489
- async loadMesh(url, assetPath, onLoad, onProgress, onError) {
490
- try {
491
- // Open pack if URL provided
492
- if (url) {
493
- await this.openPack(url);
494
- }
495
- // Find the mesh asset
496
- const assetIndex = this.reader.findAssetByPath(assetPath);
497
- if (assetIndex < 0) {
498
- throw new Error(`Asset not found: ${assetPath}`);
499
- }
500
- // Read mesh data and metadata
501
- const data = this.reader.readAssetData(assetIndex);
502
- const metadata = this.reader.readAssetMetadata(assetIndex);
503
- if (!data) {
504
- throw new Error(`Failed to read mesh data for ${assetPath}`);
505
- }
506
- if (!metadata) {
507
- throw new Error(`No metadata available for ${assetPath}`);
508
- }
509
- // Parse mesh data
510
- const parsedData = MeshParser.parseMeshData(metadata, data);
511
- // Load textures for materials
512
- await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
513
- // Build Three.js scene with Draco decoder
514
- const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
515
- if (onLoad) {
516
- onLoad(scene);
517
- }
518
- return scene;
519
- }
520
- catch (error) {
521
- if (onError) {
522
- onError(error);
523
- }
524
- throw error;
525
- }
464
+ readAssetData(index) {
465
+ return this.reader.readAssetData(index);
526
466
  }
527
467
  /**
528
- * Load textures for materials
468
+ * Read asset metadata by index
529
469
  */
470
+ readAssetMetadata(index) {
471
+ return this.reader.readAssetMetadata(index);
472
+ }
473
+ /**
474
+ * Parse texture metadata
475
+ */
476
+ parseTextureMetadata(metadataBytes) {
477
+ return this.reader.parseTextureMetadata(metadataBytes);
478
+ }
479
+ /**
480
+ * Load mesh by index
481
+ */
482
+ async loadMeshByIndex(index) {
483
+ const data = this.reader.readAssetData(index);
484
+ const metadata = this.reader.readAssetMetadata(index);
485
+ if (!data)
486
+ throw new Error(`Failed to read mesh data for index ${index}`);
487
+ if (!metadata)
488
+ throw new Error(`No metadata for index ${index}`);
489
+ const parsedData = MeshParser.parseMeshData(metadata, data);
490
+ await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
491
+ return await MeshParser.buildScene(parsedData, data, this.dracoLoader);
492
+ }
493
+ /**
494
+ * Load texture by index
495
+ */
496
+ async loadTextureByIndex(index) {
497
+ const data = this.reader.readAssetData(index);
498
+ if (!data)
499
+ throw new Error(`Failed to read texture for index ${index}`);
500
+ return await this.loadKTX2Texture(data);
501
+ }
502
+ /**
503
+ * Create HTML audio preview element
504
+ */
505
+ async createAudioPreview(index) {
506
+ const data = this.reader.readAssetData(index);
507
+ if (!data)
508
+ throw new Error(`Failed to read audio for index ${index}`);
509
+ const blob = new Blob([data], { type: 'audio/ogg' });
510
+ const url = URL.createObjectURL(blob);
511
+ const audio = document.createElement('audio');
512
+ audio.controls = true;
513
+ audio.src = url;
514
+ audio.addEventListener('ended', () => URL.revokeObjectURL(url));
515
+ audio.addEventListener('error', () => URL.revokeObjectURL(url));
516
+ return audio;
517
+ }
518
+ /**
519
+ * Get audio metadata by path
520
+ */
521
+ getAudioMetadata(assetPath) {
522
+ const assetIndex = this.reader.findAssetByPath(assetPath);
523
+ if (assetIndex < 0)
524
+ return null;
525
+ const metadata = this.reader.readAssetMetadata(assetIndex);
526
+ if (!metadata || metadata.length < 140)
527
+ return null;
528
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
529
+ const decoder = new TextDecoder();
530
+ const stringIdBytes = metadata.slice(0, 128);
531
+ const nullIdx = stringIdBytes.indexOf(0);
532
+ const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
533
+ return {
534
+ stringId,
535
+ sampleRate: view.getUint32(128, true),
536
+ channels: view.getUint32(132, true),
537
+ durationMs: view.getUint32(136, true)
538
+ };
539
+ }
540
+ /**
541
+ * Load material schema by index
542
+ */
543
+ loadMaterialSchemaByIndex(index) {
544
+ return this.loadMaterialSchemaByIndex_internal(index);
545
+ }
546
+ /**
547
+ * Get mesh materials by index
548
+ */
549
+ getMeshMaterialsByIndex(index) {
550
+ return this.getMeshMaterialsByIndex_internal(index);
551
+ }
552
+ /**
553
+ * Get material schema information
554
+ */
555
+ getMaterialSchema(assetPath) {
556
+ const assetIndex = this.reader.findAssetByPath(assetPath);
557
+ if (assetIndex < 0)
558
+ return null;
559
+ return this.loadMaterialSchemaByIndex_internal(assetIndex);
560
+ }
561
+ /**
562
+ * Get mesh materials information
563
+ */
564
+ getMeshMaterials(assetPath) {
565
+ const assetIndex = this.reader.findAssetByPath(assetPath);
566
+ if (assetIndex < 0)
567
+ return null;
568
+ return this.getMeshMaterialsByIndex_internal(assetIndex);
569
+ }
570
+ /**
571
+ * Close the pack and free resources
572
+ */
573
+ dispose() {
574
+ this.reader.close();
575
+ }
576
+ // Private methods moved from StowKitLoader
530
577
  async loadMaterialTextures(materialData, materials) {
531
578
  for (let i = 0; i < materialData.length; i++) {
532
579
  const matData = materialData[i];
@@ -534,21 +581,7 @@ class StowKitLoader extends THREE.Loader {
534
581
  for (const prop of matData.properties) {
535
582
  if (prop.textureId && prop.textureId.length > 0) {
536
583
  try {
537
- // Find texture asset
538
- const textureIndex = this.reader.findAssetByPath(prop.textureId);
539
- if (textureIndex < 0) {
540
- console.warn(`Texture not found: ${prop.textureId}`);
541
- continue;
542
- }
543
- // Read texture data
544
- const textureData = this.reader.readAssetData(textureIndex);
545
- if (!textureData) {
546
- console.warn(`Failed to read texture: ${prop.textureId}`);
547
- continue;
548
- }
549
- // Load as KTX2
550
- const texture = await this.loadKTX2Texture(textureData);
551
- // Apply to material based on property name
584
+ const texture = await this.loadTexture(prop.textureId);
552
585
  this.applyTextureToMaterial(material, prop.fieldName, texture);
553
586
  }
554
587
  catch (error) {
@@ -558,9 +591,6 @@ class StowKitLoader extends THREE.Loader {
558
591
  }
559
592
  }
560
593
  }
561
- /**
562
- * Load a KTX2 texture from binary data
563
- */
564
594
  async loadKTX2Texture(data) {
565
595
  const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
566
596
  const blob = new Blob([arrayBuffer]);
@@ -569,7 +599,6 @@ class StowKitLoader extends THREE.Loader {
569
599
  return await new Promise((resolve, reject) => {
570
600
  this.ktx2Loader.load(url, (texture) => {
571
601
  URL.revokeObjectURL(url);
572
- // Compressed textures don't support flipY, we flip UVs in MeshParser instead
573
602
  texture.needsUpdate = true;
574
603
  resolve(texture);
575
604
  }, undefined, (error) => {
@@ -583,9 +612,6 @@ class StowKitLoader extends THREE.Loader {
583
612
  throw error;
584
613
  }
585
614
  }
586
- /**
587
- * Apply texture to appropriate material property
588
- */
589
615
  applyTextureToMaterial(material, propertyName, texture) {
590
616
  const propNameLower = propertyName.toLowerCase();
591
617
  if (propNameLower === 'maintex' || propNameLower.includes('diffuse') ||
@@ -602,421 +628,153 @@ class StowKitLoader extends THREE.Loader {
602
628
  material.roughnessMap = texture;
603
629
  }
604
630
  else {
605
- // Default to main texture
606
631
  material.map = texture;
607
632
  }
608
633
  material.needsUpdate = true;
609
634
  }
610
- /**
611
- * Load a texture asset from a StowKit pack
612
- * @param url - URL to the .stow file (or omit if already opened with openPack)
613
- * @param assetPath - Path to the texture asset within the pack
614
- * @param onLoad - Callback when loading completes
615
- * @param onProgress - Progress callback
616
- * @param onError - Error callback
617
- */
618
- async loadTexture(url, assetPath, onLoad, onProgress, onError) {
619
- try {
620
- // Open pack if URL provided
621
- if (url) {
622
- await this.openPack(url);
623
- }
624
- // Find the texture asset
625
- const assetIndex = this.reader.findAssetByPath(assetPath);
626
- if (assetIndex < 0) {
627
- throw new Error(`Asset not found: ${assetPath}`);
628
- }
629
- return await this.loadTextureByIndex(assetIndex, onLoad, onProgress, onError);
630
- }
631
- catch (error) {
632
- if (onError) {
633
- onError(error);
634
- }
635
- throw error;
636
- }
637
- }
638
- /**
639
- * Load a texture asset by its index in the pack
640
- * @param index - Asset index
641
- * @param onLoad - Callback when loading completes
642
- * @param onProgress - Progress callback
643
- * @param onError - Error callback
644
- */
645
- async loadTextureByIndex(index, onLoad, onProgress, onError) {
646
- try {
647
- // Read texture data
648
- const data = this.reader.readAssetData(index);
649
- if (!data) {
650
- throw new Error(`Failed to read texture data for index ${index}`);
651
- }
652
- // Load as KTX2
653
- const texture = await this.loadKTX2Texture(data);
654
- if (onLoad) {
655
- onLoad(texture);
656
- }
657
- return texture;
658
- }
659
- catch (error) {
660
- if (onError) {
661
- onError(error);
662
- }
663
- throw error;
664
- }
665
- }
666
- /**
667
- * Load an audio asset from a StowKit pack
668
- * @param url - URL to the .stow file (or omit if already opened with openPack)
669
- * @param assetPath - Path to the audio asset within the pack
670
- * @param listener - THREE.AudioListener to attach to
671
- * @param onLoad - Callback when loading completes
672
- * @param onProgress - Progress callback
673
- * @param onError - Error callback
674
- */
675
- async loadAudio(url, assetPath, listener, onLoad, onProgress, onError) {
676
- try {
677
- // Open pack if URL provided
678
- if (url) {
679
- await this.openPack(url);
680
- }
681
- // Find the audio asset
682
- const assetIndex = this.reader.findAssetByPath(assetPath);
683
- if (assetIndex < 0) {
684
- throw new Error(`Asset not found: ${assetPath}`);
685
- }
686
- // Read audio data
687
- const data = this.reader.readAssetData(assetIndex);
688
- if (!data) {
689
- throw new Error(`Failed to read audio data for ${assetPath}`);
690
- }
691
- // Create audio object
692
- const audio = new THREE.Audio(listener);
693
- // Decode audio data
694
- const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
695
- const audioContext = listener.context;
696
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
697
- audio.setBuffer(audioBuffer);
698
- if (onLoad) {
699
- onLoad(audio);
700
- }
701
- return audio;
702
- }
703
- catch (error) {
704
- if (onError) {
705
- onError(error);
706
- }
707
- throw error;
708
- }
709
- }
710
- /**
711
- * Get metadata for an audio asset
712
- * @param assetPath - Path to the audio asset within the pack
713
- */
714
- getAudioMetadata(assetPath) {
715
- try {
716
- const assetIndex = this.reader.findAssetByPath(assetPath);
717
- if (assetIndex < 0)
718
- return null;
719
- const metadata = this.reader.readAssetMetadata(assetIndex);
720
- if (!metadata || metadata.length < 140)
721
- return null;
722
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
723
- const decoder = new TextDecoder();
724
- // Parse AudioMetadata structure
725
- const stringIdBytes = metadata.slice(0, 128);
726
- const nullIdx = stringIdBytes.indexOf(0);
727
- const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
728
- return {
729
- stringId,
730
- sampleRate: view.getUint32(128, true),
731
- channels: view.getUint32(132, true),
732
- durationMs: view.getUint32(136, true)
733
- };
734
- }
735
- catch (error) {
736
- console.error('Failed to read audio metadata:', error);
737
- return null;
738
- }
739
- }
740
- /**
741
- * Get metadata for a texture asset
742
- * @param assetPath - Path to the texture asset within the pack
743
- */
744
- getTextureMetadata(assetPath) {
745
- try {
746
- const assetIndex = this.reader.findAssetByPath(assetPath);
747
- if (assetIndex < 0)
748
- return null;
749
- const metadata = this.reader.readAssetMetadata(assetIndex);
750
- if (!metadata || metadata.length < 144)
751
- return null; // TextureMetadata is now 144 bytes
752
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
753
- const decoder = new TextDecoder();
754
- // Parse TextureMetadata structure (width, height, channels, channel_format, string_id[128])
755
- const stringIdBytes = metadata.slice(16, 144); // After 4 uint32s (16 bytes)
756
- const nullIdx = stringIdBytes.indexOf(0);
757
- const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
758
- return {
759
- width: view.getUint32(0, true),
760
- height: view.getUint32(4, true),
761
- channels: view.getUint32(8, true),
762
- isKtx2: true, // All textures are KTX2 now
763
- channelFormat: view.getUint32(12, true),
764
- stringId
765
- };
766
- }
767
- catch (error) {
768
- console.error('Failed to read texture metadata:', error);
635
+ loadMaterialSchemaByIndex_internal(index) {
636
+ const metadata = this.reader.readAssetMetadata(index);
637
+ if (!metadata || metadata.length < 196)
769
638
  return null;
639
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
640
+ const decoder = new TextDecoder();
641
+ const stringIdBytes = metadata.slice(0, 128);
642
+ const schemaNameBytes = metadata.slice(128, 192);
643
+ const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
644
+ const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
645
+ const fieldCount = view.getUint32(192, true);
646
+ const fields = [];
647
+ let offset = 196;
648
+ for (let i = 0; i < fieldCount; i++) {
649
+ if (offset + 88 > metadata.length)
650
+ break;
651
+ const fieldNameBytes = metadata.slice(offset, offset + 64);
652
+ const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
653
+ const fieldType = view.getUint32(offset + 64, true);
654
+ const previewFlags = view.getUint32(offset + 68, true);
655
+ const defaultValue = [
656
+ view.getFloat32(offset + 72, true),
657
+ view.getFloat32(offset + 76, true),
658
+ view.getFloat32(offset + 80, true),
659
+ view.getFloat32(offset + 84, true)
660
+ ];
661
+ const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
662
+ const previewFlagNames = ['None', 'MainTex', 'Tint'];
663
+ fields.push({
664
+ name: fieldName,
665
+ type: typeNames[fieldType] || 'Float',
666
+ previewFlag: previewFlagNames[previewFlags] || 'None',
667
+ defaultValue
668
+ });
669
+ offset += 88;
770
670
  }
671
+ return { stringId, schemaName, fields };
771
672
  }
772
- /**
773
- * Load material schema information by index
774
- * @param index - Asset index
775
- */
776
- loadMaterialSchemaByIndex(index) {
777
- try {
778
- const metadata = this.reader.readAssetMetadata(index);
779
- if (!metadata || metadata.length < 196)
780
- return null;
781
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
782
- const decoder = new TextDecoder();
783
- const stringIdBytes = metadata.slice(0, 128);
784
- const schemaNameBytes = metadata.slice(128, 192);
785
- const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
786
- const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
787
- const fieldCount = view.getUint32(192, true);
788
- const fields = [];
789
- let offset = 196;
790
- for (let i = 0; i < fieldCount; i++) {
791
- if (offset + 88 > metadata.length)
792
- break;
793
- const fieldNameBytes = metadata.slice(offset, offset + 64);
794
- const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
795
- const fieldType = view.getUint32(offset + 64, true);
796
- const previewFlags = view.getUint32(offset + 68, true);
797
- const defaultValue = [
798
- view.getFloat32(offset + 72, true),
799
- view.getFloat32(offset + 76, true),
800
- view.getFloat32(offset + 80, true),
801
- view.getFloat32(offset + 84, true)
802
- ];
803
- const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
804
- const previewFlagNames = ['None', 'MainTex', 'Tint'];
805
- fields.push({
806
- name: fieldName,
807
- type: typeNames[fieldType] || 'Float',
808
- previewFlag: previewFlagNames[previewFlags] || 'None',
809
- defaultValue
810
- });
811
- offset += 88;
812
- }
813
- return { stringId, schemaName, fields };
814
- }
815
- catch (error) {
816
- console.error('Failed to read material schema:', error);
673
+ getMeshMaterialsByIndex_internal(index) {
674
+ const metadata = this.reader.readAssetMetadata(index);
675
+ if (!metadata || metadata.length < 140)
817
676
  return null;
818
- }
819
- }
820
- /**
821
- * Load material schema information by path
822
- * @param assetPath - Path to the material schema asset
823
- */
824
- loadMaterialSchema(assetPath) {
825
- try {
826
- const assetIndex = this.reader.findAssetByPath(assetPath);
827
- if (assetIndex < 0)
828
- return null;
829
- const metadata = this.reader.readAssetMetadata(assetIndex);
830
- if (!metadata || metadata.length < 196)
831
- return null;
832
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
833
- const decoder = new TextDecoder();
834
- // Parse MaterialSchemaMetadata: string_id[128] + schema_name[64] + field_count(4) = 196 bytes
835
- const stringIdBytes = metadata.slice(0, 128);
836
- const schemaNameBytes = metadata.slice(128, 192);
837
- const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
838
- const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
839
- const fieldCount = view.getUint32(192, true);
840
- const fields = [];
841
- let offset = 196;
842
- // Parse fields: name[64] + field_type(4) + preview_flags(4) + default_value[4*4=16] = 88 bytes
843
- for (let i = 0; i < fieldCount; i++) {
844
- if (offset + 88 > metadata.length)
677
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
678
+ const decoder = new TextDecoder();
679
+ const meshGeometryCount = view.getUint32(0, true);
680
+ const materialCount = view.getUint32(4, true);
681
+ const nodeCount = view.getUint32(8, true);
682
+ let offset = 140;
683
+ offset += meshGeometryCount * 40;
684
+ const materials = [];
685
+ for (let i = 0; i < materialCount; i++) {
686
+ if (offset + 196 > metadata.length)
687
+ break;
688
+ const nameBytes = metadata.slice(offset, offset + 64);
689
+ const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
690
+ const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
691
+ const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
692
+ const propertyCount = view.getUint32(offset + 192, true);
693
+ materials.push({ name, schemaId, propertyCount, properties: [] });
694
+ offset += 196;
695
+ }
696
+ offset += nodeCount * 116;
697
+ offset += nodeCount * 4;
698
+ for (const mat of materials) {
699
+ for (let j = 0; j < mat.propertyCount; j++) {
700
+ if (offset + 144 > metadata.length)
845
701
  break;
846
702
  const fieldNameBytes = metadata.slice(offset, offset + 64);
847
703
  const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
848
- const fieldType = view.getUint32(offset + 64, true);
849
- const previewFlags = view.getUint32(offset + 68, true);
850
- const defaultValue = [
704
+ const value = [
705
+ view.getFloat32(offset + 64, true),
706
+ view.getFloat32(offset + 68, true),
851
707
  view.getFloat32(offset + 72, true),
852
- view.getFloat32(offset + 76, true),
853
- view.getFloat32(offset + 80, true),
854
- view.getFloat32(offset + 84, true)
708
+ view.getFloat32(offset + 76, true)
855
709
  ];
856
- const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
857
- const previewFlagNames = ['None', 'MainTex', 'Tint'];
858
- fields.push({
859
- name: fieldName,
860
- type: typeNames[fieldType] || 'Float',
861
- previewFlag: previewFlagNames[previewFlags] || 'None',
862
- defaultValue
863
- });
864
- offset += 88; // MaterialSchemaField is 88 bytes total
865
- }
866
- return { stringId, schemaName, fields };
867
- }
868
- catch (error) {
869
- console.error('Failed to read material schema:', error);
870
- return null;
871
- }
872
- }
873
- /**
874
- * Get material information from a loaded mesh
875
- * Returns material data including properties and texture references
876
- */
877
- getMeshMaterials(assetPath) {
878
- try {
879
- const assetIndex = this.reader.findAssetByPath(assetPath);
880
- if (assetIndex < 0)
881
- return null;
882
- const metadata = this.reader.readAssetMetadata(assetIndex);
883
- if (!metadata || metadata.length < 140)
884
- return null;
885
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
886
- const decoder = new TextDecoder();
887
- // Parse MeshMetadata
888
- const meshGeometryCount = view.getUint32(0, true);
889
- const materialCount = view.getUint32(4, true);
890
- const nodeCount = view.getUint32(8, true);
891
- let offset = 140;
892
- // Skip geometries (40 bytes each - Draco compressed)
893
- offset += meshGeometryCount * 40;
894
- // Parse materials
895
- const materials = [];
896
- for (let i = 0; i < materialCount; i++) {
897
- if (offset + 196 > metadata.length)
898
- break;
899
- const nameBytes = metadata.slice(offset, offset + 64);
900
- const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
901
- const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
902
- const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
903
- const propertyCount = view.getUint32(offset + 192, true);
904
- materials.push({ name, schemaId, propertyCount, properties: [] });
905
- offset += 196;
906
- }
907
- // Skip nodes (116 bytes each)
908
- offset += nodeCount * 116;
909
- // Skip mesh indices (4 bytes each)
910
- const totalMeshRefs = materials.reduce((sum, mat) => {
911
- // This is a simplification - we'd need to parse nodes to get exact count
912
- return sum;
913
- }, 0);
914
- // For now, skip based on nodeCount assumption
915
- offset += nodeCount * 4;
916
- // Parse material properties
917
- for (const mat of materials) {
918
- for (let j = 0; j < mat.propertyCount; j++) {
919
- if (offset + 144 > metadata.length)
920
- break;
921
- const fieldNameBytes = metadata.slice(offset, offset + 64);
922
- const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
923
- const value = [
924
- view.getFloat32(offset + 64, true),
925
- view.getFloat32(offset + 68, true),
926
- view.getFloat32(offset + 72, true),
927
- view.getFloat32(offset + 76, true)
928
- ];
929
- const textureIdBytes = metadata.slice(offset + 80, offset + 144);
930
- const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
931
- mat.properties.push({ fieldName, value, textureId });
932
- offset += 144;
933
- }
710
+ const textureIdBytes = metadata.slice(offset + 80, offset + 144);
711
+ const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
712
+ mat.properties.push({ fieldName, value, textureId });
713
+ offset += 144;
934
714
  }
935
- return materials;
936
- }
937
- catch (error) {
938
- console.error('Failed to read mesh materials:', error);
939
- return null;
940
715
  }
716
+ return materials;
941
717
  }
718
+ }
719
+
720
+ /**
721
+ * Three.js loader for StowKit asset packs
722
+ *
723
+ * Usage:
724
+ * ```typescript
725
+ * const pack = await StowKitLoader.load('assets.stow');
726
+ * const mesh = await pack.loadMesh('models/character');
727
+ * const texture = await pack.loadTexture('textures/diffuse');
728
+ * const audio = await pack.loadAudio('sounds/bgm', listener);
729
+ * ```
730
+ */
731
+ class StowKitLoader {
942
732
  /**
943
- * Open a .stow file from a File or ArrayBuffer
733
+ * Load a .stow pack file
944
734
  */
945
- async open(fileOrBuffer) {
946
- // Initialize reader if needed
947
- if (this.ownedReader) {
948
- await this.reader.init();
735
+ static async load(url, options) {
736
+ // Initialize loaders if needed
737
+ if (!this.initialized) {
738
+ await this.initialize(options);
949
739
  }
950
- return await this.reader.open(fileOrBuffer);
951
- }
952
- /**
953
- * Get the total number of assets
954
- */
955
- getAssetCount() {
956
- return this.reader.getAssetCount();
957
- }
958
- /**
959
- * Get info about a specific asset
960
- */
961
- getAssetInfo(index) {
962
- return this.reader.getAssetInfo(index);
963
- }
964
- /**
965
- * Read asset data by index
966
- */
967
- readAssetData(index) {
968
- return this.reader.readAssetData(index);
969
- }
970
- /**
971
- * Read asset metadata by index
972
- */
973
- readAssetMetadata(index) {
974
- return this.reader.readAssetMetadata(index);
975
- }
976
- /**
977
- * Parse texture metadata
978
- */
979
- parseTextureMetadata(metadataBytes) {
980
- return this.reader.parseTextureMetadata(metadataBytes);
981
- }
982
- /**
983
- * Create an HTML audio element for preview
984
- * @param index - Asset index
985
- */
986
- async createAudioPreview(index) {
987
- const data = this.reader.readAssetData(index);
988
- if (!data) {
989
- throw new Error(`Failed to read audio data for index ${index}`);
740
+ // Fetch the pack file
741
+ const response = await fetch(url);
742
+ if (!response.ok) {
743
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
990
744
  }
991
- // Create blob URL for HTML5 audio
992
- const blob = new Blob([data], { type: 'audio/ogg' });
993
- const url = URL.createObjectURL(blob);
994
- // Create and configure audio element
995
- const audio = document.createElement('audio');
996
- audio.controls = true;
997
- audio.src = url;
998
- // Clean up URL when audio ends or on error
999
- audio.addEventListener('ended', () => URL.revokeObjectURL(url));
1000
- audio.addEventListener('error', () => URL.revokeObjectURL(url));
1001
- return audio;
1002
- }
1003
- /**
1004
- * List all assets in the opened pack
1005
- */
1006
- listAssets() {
1007
- return this.reader.listAssets();
745
+ const arrayBuffer = await response.arrayBuffer();
746
+ // Open pack
747
+ await this.reader.open(arrayBuffer);
748
+ // Return pack wrapper
749
+ return new StowKitPack(this.reader, this.ktx2Loader, this.dracoLoader);
1008
750
  }
1009
751
  /**
1010
- * Dispose of resources
752
+ * Initialize the loader (called automatically on first load)
1011
753
  */
1012
- dispose() {
1013
- this.ktx2Loader.dispose();
1014
- this.dracoLoader.dispose();
1015
- if (this.ownedReader) {
1016
- this.reader.close();
1017
- }
754
+ static async initialize(options) {
755
+ const wasmPath = options?.wasmPath || '/stowkit_reader.wasm';
756
+ const basisPath = options?.basisPath || '/stowkit/basis/';
757
+ const dracoPath = options?.dracoPath || '/stowkit/draco/';
758
+ // Initialize reader
759
+ this.reader = new StowKitReader(wasmPath);
760
+ await this.reader.init();
761
+ // Setup KTX2 loader
762
+ this.ktx2Loader = new KTX2Loader();
763
+ this.ktx2Loader.setTranscoderPath(basisPath);
764
+ // Setup Draco loader
765
+ this.dracoLoader = new DRACOLoader();
766
+ this.dracoLoader.setDecoderPath(dracoPath);
767
+ // Detect support
768
+ const tempRenderer = new THREE.WebGLRenderer();
769
+ this.ktx2Loader.detectSupport(tempRenderer);
770
+ tempRenderer.dispose();
771
+ this.initialized = true;
1018
772
  }
1019
773
  }
774
+ StowKitLoader.reader = null;
775
+ StowKitLoader.ktx2Loader = null;
776
+ StowKitLoader.dracoLoader = null;
777
+ StowKitLoader.initialized = false;
1020
778
 
1021
- export { MeshParser, StowKitLoader };
779
+ export { StowKitLoader, StowKitPack };
1022
780
  //# sourceMappingURL=stowkit-three-loader.esm.js.map