@stowkit/three-loader 0.1.10 → 0.1.12

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