npm package: @stowkit/three-loader
Description: Three.js loader for StowKit asset packs
Latest version: 0.1.40

# @stowkit/three-loader

Three.js loader for StowKit asset packs. Provides a simple, high-level API for loading meshes, skinned meshes, animations, textures, and audio from `.stow` files.

## Installation

```bash
npm install @stowkit/three-loader three
```

## Quick Start

```typescript
import { StowKitLoader, AssetType } from '@stowkit/three-loader';

const pack = await StowKitLoader.load('assets.stow');
// from memory
const packFromMemory = await StowKitLoader.loadFromMemory(someData);

const mesh = await pack.loadMesh('character');
scene.add(mesh);

const character = await pack.loadSkinnedMesh('player');
scene.add(character);

const { mixer } = await pack.loadAnimation(character, 'walk');

// Update in your animation loop
function animate() {
    const delta = clock.getDelta();
    mixer.update(delta);
    renderer.render(scene, camera);
}
```

## Features

- ✅ **Static Meshes** - Draco-compressed meshes with automatic material/texture loading
- ✅ **Skinned Meshes** - Skeletal meshes with bone hierarchy
- ✅ **Animations** - Skeletal animations with automatic mixer setup
- ✅ **Textures** - KTX2/Basis Universal GPU-compressed textures
- ✅ **Audio** - OGG/MP3 audio with Three.js Audio integration
- ✅ **Multiple Packs** - Load multiple .stow files simultaneously with isolated state
- ✅ **WASM Parsing** - All binary parsing done in WASM for performance
- ✅ **Type Safe** - Full TypeScript support
- ✅ **Zero Config** - Works out of the box

## API Reference

### Asset Manifest

#### `pack.listAssets(): AssetListItem[]`

Get the complete manifest of all assets in the pack.

```typescript
const pack = await StowKitLoader.load('assets.stow');
const manifest = pack.listAssets();

console.log(`Pack contains ${manifest.length} assets`);

manifest.forEach(asset => {
    console.log(`[${asset.index}] ${asset.name || asset.id}`);
    console.log(`  Type: ${getTypeName(asset.type)}`);
    console.log(`  Size: ${formatBytes(asset.dataSize)}`);
    console.log(`  Has Metadata: ${asset.hasMetadata}`);
});

// Filter by type
const meshes = manifest.filter(a => a.type === AssetType.STATIC_MESH);
const textures = manifest.filter(a => a.type === AssetType.TEXTURE_2D);
const audio = manifest.filter(a => a.type === AssetType.AUDIO);
const skinnedMeshes = manifest.filter(a => a.type === AssetType.SKINNED_MESH);
const animations = manifest.filter(a => a.type === AssetType.ANIMATION_CLIP);

console.log(`${meshes.length} meshes, ${textures.length} textures, ${animations.length} animations`);
```

**AssetListItem structure:**
```typescript
{
    index: number;           // Asset index
    type: number;            // Asset type ID (1-6)
    name?: string;           // Extracted from metadata (if available)
    id: bigint;              // Unique asset ID
    dataSize: number;        // Size of asset data in bytes
    metadataSize: number;    // Size of metadata in bytes
    hasMetadata: boolean;    // Whether metadata exists
    data_offset: number;     // Internal use
    data_size: number;       // Internal use
    metadata_offset: number; // Internal use
    metadata_size: number;   // Internal use
}
```

#### `pack.getAssetCount(): number`

Get total number of assets in the pack.

```typescript
const count = pack.getAssetCount();
console.log(`Pack has ${count} assets`);
```

#### `pack.getAssetInfo(index: number): AssetInfo | null`

Get detailed info about a specific asset.

```typescript
const info = pack.getAssetInfo(5);
if (info) {
    console.log(`Type: ${info.type}, Size: ${info.data_size} bytes`);
}
```

### Loading Assets

#### Static Meshes

```typescript
// Load by string ID
const mesh = await pack.loadMesh('models/building.mesh');
scene.add(mesh);

// Load by index
const mesh = await pack.loadMeshByIndex(5);
scene.add(mesh);
```

Returns a `THREE.Group` containing the mesh hierarchy with materials and textures applied.

#### Skinned Meshes

```typescript
// Load by string ID
const character = await pack.loadSkinnedMesh('characters/player.skinned');
scene.add(character);

// Load by index
const character = await pack.loadSkinnedMeshByIndex(8);
scene.add(character);
```

Returns a `THREE.Group` containing the skinned mesh with skeleton and bones in bind pose.

#### Animations

```typescript
// Load and play animation (returns mixer, action, clip)
const { mixer, action, clip } = await pack.loadAnimation(
    skinnedMeshGroup,
    'animations/walk.anim'
);

// Or by index
const { mixer, action, clip } = await pack.loadAnimationByIndex(
    skinnedMeshGroup,
    9
);

// Update in your animation loop
const clock = new THREE.Clock();
function animate() {
    requestAnimationFrame(animate);
    mixer.update(clock.getDelta());
    renderer.render(scene, camera);
}
```

The `loadAnimation` methods automatically:
- Create an `AnimationMixer` on the correct root object
- Set the animation to loop infinitely
- Start playing immediately
- Return everything you need

#### Textures

```typescript
// Load by string ID
const texture = await pack.loadTexture('textures/wood.ktx2');
material.map = texture;

// Load by index
const texture = await pack.loadTextureByIndex(2);
```

Returns a `THREE.CompressedTexture` (KTX2/Basis Universal format).

#### Audio

```typescript
const listener = new THREE.AudioListener();
camera.add(listener);

// Load by string ID
const bgm = await pack.loadAudio('sounds/music.ogg', listener);
bgm.setLoop(true);
bgm.play();

// Or create HTML5 audio element for preview
const audioElement = await pack.createAudioPreview(3);
document.body.appendChild(audioElement);
```

### Metadata Helpers

All metadata is parsed in WASM for performance and reliability.

#### Animation Metadata

```typescript
const animData = pack.getAnimationMetadata(index);

console.log(animData.stringId);        // "Clip_Walking"
console.log(animData.targetMeshId);    // "Character_Skinned_Tpose"
console.log(animData.duration);        // 0.97
console.log(animData.ticksPerSecond);  // 30
console.log(animData.channelCount);    // 104
console.log(animData.boneCount);       // 65
```

#### Audio Metadata

```typescript
const audioData = pack.getAudioMetadata('sounds/bgm.ogg');

console.log(audioData.sampleRate);  // 44100
console.log(audioData.channels);    // 2 (stereo)
console.log(audioData.durationMs);  // 180000 (3 minutes)
```

#### Texture Metadata

```typescript
const texData = pack.getTextureMetadata(index);

console.log(texData.width);          // 1024
console.log(texData.height);         // 1024
console.log(texData.channels);       // 3 (RGB) or 4 (RGBA)
console.log(texData.channelFormat);  // 1 (RGB) or 2 (RGBA)
```

### Asset Discovery

```typescript
// List all assets in pack
const assets = pack.listAssets();

assets.forEach(asset => {
    console.log(`[${asset.index}] ${asset.name} (${getTypeName(asset.type)})`);
});

// Find asset by path
const index = pack.reader.findAssetByPath('models/character.mesh');

// Get asset count
const count = pack.getAssetCount();

// Get asset info
const info = pack.getAssetInfo(5);
```

## Complete Example

```typescript
import * as THREE from 'three';
import { StowKitLoader } from '@stowkit/three-loader';

// Setup Three.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
const clock = new THREE.Clock();

const listener = new THREE.AudioListener();
camera.add(listener);

// Load pack
const pack = await StowKitLoader.load('game.stow');

// Load static mesh
const environment = await pack.loadMesh('levels/level1.mesh');
scene.add(environment);

// Load skinned character
const character = await pack.loadSkinnedMesh('characters/player.skinned');
character.position.set(0, 0, 0);
scene.add(character);

// Load and play animation
const { mixer } = await pack.loadAnimation(character, 'animations/idle.anim');

// Load audio
const bgm = await pack.loadAudio('sounds/theme.ogg', listener);
bgm.setLoop(true);
bgm.play();

// Animation loop
function animate() {
    requestAnimationFrame(animate);
    mixer.update(clock.getDelta());
    renderer.render(scene, camera);
}

animate();
```

## Loading Multiple Packs

Each pack is fully isolated with its own state. You can load multiple packs simultaneously without any interference:

```typescript
// Load multiple packs at once
const [environmentPack, characterPack, audioPack] = await Promise.all([
    StowKitLoader.load('environment.stow'),
    StowKitLoader.load('characters.stow'),
    StowKitLoader.load('audio.stow')
]);

// Load assets from different packs
const level = await environmentPack.loadMesh('level1');
const player = await characterPack.loadSkinnedMesh('player');
const bgm = await audioPack.loadAudio('theme', listener);

scene.add(level);
scene.add(player);
bgm.play();

// Each pack maintains its own asset catalog
console.log(`Environment: ${environmentPack.getAssetCount()} assets`);
console.log(`Characters: ${characterPack.getAssetCount()} assets`);
console.log(`Audio: ${audioPack.getAssetCount()} assets`);
```

**Note:** Each pack creates its own WASM instance for isolated state. Dispose packs when no longer needed:

```typescript
environmentPack.dispose();
```

## Asset Types

```typescript
import { AssetType } from '@stowkit/three-loader';
```

| Enum | Value | Description |
|------|-------|-------------|
| `AssetType.STATIC_MESH` | 1 | Draco-compressed 3D models |
| `AssetType.TEXTURE_2D` | 2 | KTX2/Basis Universal textures |
| `AssetType.AUDIO` | 3 | OGG/MP3 audio files |
| `AssetType.MATERIAL_SCHEMA` | 4 | Material template definitions |
| `AssetType.SKINNED_MESH` | 5 | Skeletal meshes with bones |
| `AssetType.ANIMATION_CLIP` | 6 | Bone animation keyframes |

## Public Folder Setup

The package automatically copies required files on install:

```
public/
└── stowkit/
    ├── stowkit_reader.wasm    # WASM reader module
    ├── basis/                  # Basis Universal transcoder
    │   ├── basis_transcoder.js
    │   └── basis_transcoder.wasm
    └── draco/                  # Draco decoder
        ├── draco_decoder.js
        ├── draco_decoder.wasm
        └── draco_wasm_wrapper.js
```

Just run `npm install` and everything is set up automatically!

## Performance

- **WASM Parsing**: All binary parsing done in WASM (10-50x faster than JavaScript)
- **Draco Compression**: Meshes are 80-90% smaller than uncompressed
- **KTX2 Textures**: GPU-native compression, fast loading and rendering
- **Lazy Loading**: Assets loaded on-demand, not all at once
- **Memory Efficient**: Minimal copying between WASM and JavaScript

## Troubleshooting

### Animations appear broken/offset from origin

Make sure your `.stow` file was packed with the latest packer that writes bone parent indices correctly.

### Textures not loading

Ensure `/stowkit/basis/` folder contains the Basis Universal transcoder files (auto-copied on install).

### Audio not playing

Make sure you've created an `AudioListener` and attached it to your camera.