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

# @series-inc/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 @series-inc/stowkit-three-loader three
```

## Quick Start

```typescript
import { StowKitLoader, AssetType } from '@series-inc/stowkit-three-loader';

const pack = await StowKitLoader.load('assets.stow');

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');

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** - AAC audio with Three.js Audio integration
- **Multiple Packs** - Load multiple .stow files simultaneously with isolated state
- **WASM Acceleration** - Binary parsing and skinned mesh geometry decoded via WASM
- **Type Safe** - Full TypeScript support
- **Zero Config** - Works out of the box

## API Reference

### Exports

```typescript
export { StowKitLoader } from '@series-inc/stowkit-three-loader';
export { StowKitPack } from '@series-inc/stowkit-three-loader';
export { AssetMemoryCache } from '@series-inc/stowkit-three-loader';
export { AssetType, PerfLogger } from '@series-inc/stowkit-three-loader';

export type { StowKitLoaderOptions } from '@series-inc/stowkit-three-loader';
export type { MeshGeometryInfo, MaterialPropertyValue, MaterialData, Node, MeshMetadata } from '@series-inc/stowkit-three-loader';
```

### StowKitLoader

Static loader class for opening `.stow` pack files.

#### `StowKitLoader.load(url, options?)`

Loads a `.stow` pack from a URL.

```typescript
const pack = await StowKitLoader.load('assets.stow', {
    wasmPath: '/stowkit/stowkit_reader.wasm',
    basisPath: '/stowkit/basis/',
    dracoPath: '/stowkit/draco/'
});
```

**Options:**
- `wasmPath` (string) - Path to WASM reader module (default: `'/stowkit/stowkit_reader.wasm'`)
- `basisPath` (string) - Path to Basis Universal transcoder files (default: `'/stowkit/basis/'`)
- `dracoPath` (string) - Path to Draco decoder files (default: `'/stowkit/draco/'`)

**Returns:** `Promise<StowKitPack>`

#### `StowKitLoader.loadFromMemory(data, options?, cacheKey?)`

Loads a `.stow` pack from memory.

```typescript
const response = await fetch('assets.stow');
const buffer = await response.arrayBuffer();
const pack = await StowKitLoader.loadFromMemory(buffer);

// With a cache key for CDN-loaded files
const pack = await StowKitLoader.loadFromMemory(buffer, undefined, 'https://cdn.example.com/assets.stow');
```

**Parameters:**
- `data` (ArrayBuffer | Blob | File) - The `.stow` file data
- `options` (StowKitLoaderOptions, optional) - Loader options
- `cacheKey` (string, optional) - Unique identifier for caching (recommended for CDN-loaded files)

**Returns:** `Promise<StowKitPack>`

### StowKitPack

Represents an opened `.stow` pack with methods to load assets.

### 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: ${asset.type}`);
    console.log(`  Size: ${asset.dataSize} bytes`);
    console.log(`  Has Metadata: ${asset.hasMetadata}`);
});

const meshes = manifest.filter(a => a.type === AssetType.STATIC_MESH);
const textures = manifest.filter(a => a.type === AssetType.TEXTURE_2D);
const animations = manifest.filter(a => a.type === AssetType.ANIMATION_CLIP);
```

#### `pack.getAssetCount(): number`

Get total number of assets in the pack.

#### `pack.getAssetInfo(index: number): AssetInfo | null`

Get detailed info about a specific asset.

#### `pack.readAssetData(index: number): Uint8Array | null`

Read raw binary data of an asset by index.

#### `pack.readAssetMetadata(index: number): Uint8Array | null`

Read raw metadata bytes of an asset by index.

### 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
- Retarget bone names if they don't match exactly
- Return the mixer, action, and clip

#### Animation Clips (Without Playback)

```typescript
// Load just the clip by string ID
const clip = await pack.loadAnimationClip('animations/walk.anim');

// Or by index
const clip = await pack.loadAnimationClipByIndex(9);
```

Returns a `THREE.AnimationClip` without creating a mixer or starting playback.

#### 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). Textures are cached per-pack so loading the same texture twice returns the same instance.

#### Audio

```typescript
const listener = new THREE.AudioListener();
camera.add(listener);

// Load by string ID
const bgm = await pack.loadAudio('sounds/music', 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');

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)
```

### Cleanup

#### `pack.dispose()`

Close the pack and free resources. Clears the texture cache and closes the underlying WASM reader.

```typescript
pack.dispose();
```

## Complete Example

```typescript
import * as THREE from 'three';
import { StowKitLoader } from '@series-inc/stowkit-three-loader';

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);

const pack = await StowKitLoader.load('game.stow');

const environment = await pack.loadMesh('levels/level1.mesh');
scene.add(environment);

const character = await pack.loadSkinnedMesh('characters/player.skinned');
character.position.set(0, 0, 0);
scene.add(character);

const { mixer } = await pack.loadAnimation(character, 'animations/idle.anim');

const bgm = await pack.loadAudio('sounds/theme', listener);
bgm.setLoop(true);
bgm.play();

function animate() {
    requestAnimationFrame(animate);
    mixer.update(clock.getDelta());
    renderer.render(scene, camera);
}

animate();
```

## Loading Multiple Packs

Each pack is fully isolated with its own WASM reader instance. You can load multiple packs simultaneously without interference:

```typescript
const [environmentPack, characterPack, audioPack] = await Promise.all([
    StowKitLoader.load('environment.stow'),
    StowKitLoader.load('characters.stow'),
    StowKitLoader.load('audio.stow')
]);

const level = await environmentPack.loadMesh('level1');
const player = await characterPack.loadSkinnedMesh('player');
const bgm = await audioPack.loadAudio('theme_music', listener);

scene.add(level);
scene.add(player);
bgm.play();

console.log(`Environment: ${environmentPack.getAssetCount()} assets`);
console.log(`Characters: ${characterPack.getAssetCount()} assets`);
console.log(`Audio: ${audioPack.getAssetCount()} assets`);
```

Dispose packs when no longer needed:

```typescript
environmentPack.dispose();
```

## Asset Types

```typescript
import { AssetType } from '@series-inc/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 | AAC audio (M4A container) |
| `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
    ├── basis/
    │   ├── basis_transcoder.js
    │   └── basis_transcoder.wasm
    └── draco/
        ├── draco_decoder.js
        ├── draco_decoder.wasm
        └── draco_wasm_wrapper.js
```

Just run `npm install` and everything is set up automatically.

## Performance

- **WASM Acceleration**: Pack reading and skinned mesh geometry parsed in WASM for speed
- **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
- **Texture Caching**: Duplicate texture loads return cached promises

## 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.