@series-inc/stowkit-cli 0.1.11 → 0.1.13
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.
- package/dist/app/blob-store.d.ts +2 -0
- package/dist/app/blob-store.js +6 -0
- package/dist/app/disk-project.d.ts +30 -1
- package/dist/app/process-cache.js +12 -0
- package/dist/app/state.d.ts +4 -0
- package/dist/app/state.js +2 -0
- package/dist/app/stowmeta-io.d.ts +18 -1
- package/dist/app/stowmeta-io.js +214 -3
- package/dist/cleanup.js +2 -0
- package/dist/core/types.d.ts +2 -1
- package/dist/core/types.js +1 -0
- package/dist/encoders/fbx-loader.d.ts +2 -2
- package/dist/encoders/fbx-loader.js +140 -2
- package/dist/encoders/glb-loader.d.ts +42 -0
- package/dist/encoders/glb-loader.js +592 -0
- package/dist/encoders/interfaces.d.ts +4 -1
- package/dist/node-fs.js +2 -0
- package/dist/orchestrator.js +253 -50
- package/dist/pipeline.d.ts +20 -1
- package/dist/pipeline.js +138 -2
- package/dist/server.js +623 -121
- package/dist/workers/process-worker.d.ts +1 -0
- package/dist/workers/process-worker.js +83 -0
- package/dist/workers/worker-pool.d.ts +41 -0
- package/dist/workers/worker-pool.js +130 -0
- package/package.json +3 -3
- package/skill.md +164 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import { processAsset, processExtractedMesh } from '../pipeline.js';
|
|
3
|
+
import { BlobStore } from '../app/blob-store.js';
|
|
4
|
+
import { NodeBasisEncoder } from '../encoders/basis-encoder.js';
|
|
5
|
+
import { NodeDracoEncoder } from '../encoders/draco-encoder.js';
|
|
6
|
+
import { NodeAacEncoder, NodeAudioDecoder } from '../encoders/aac-encoder.js';
|
|
7
|
+
import { NodeFbxImporter } from '../encoders/fbx-loader.js';
|
|
8
|
+
import { SharpImageDecoder } from '../encoders/image-decoder.js';
|
|
9
|
+
// Lazy-init encoder context
|
|
10
|
+
let ctx = null;
|
|
11
|
+
let initPromise = null;
|
|
12
|
+
function getCtx(wasmDir) {
|
|
13
|
+
if (ctx)
|
|
14
|
+
return Promise.resolve(ctx);
|
|
15
|
+
if (initPromise)
|
|
16
|
+
return initPromise;
|
|
17
|
+
initPromise = (async () => {
|
|
18
|
+
const textureEncoder = new NodeBasisEncoder(wasmDir);
|
|
19
|
+
const meshEncoder = new NodeDracoEncoder();
|
|
20
|
+
const aacEncoder = new NodeAacEncoder();
|
|
21
|
+
const audioDecoder = new NodeAudioDecoder();
|
|
22
|
+
const meshImporter = new NodeFbxImporter();
|
|
23
|
+
const imageDecoder = new SharpImageDecoder();
|
|
24
|
+
await Promise.all([
|
|
25
|
+
textureEncoder.initialize(),
|
|
26
|
+
meshEncoder.initialize(),
|
|
27
|
+
aacEncoder.initialize(),
|
|
28
|
+
audioDecoder.initialize(),
|
|
29
|
+
]);
|
|
30
|
+
ctx = {
|
|
31
|
+
textureEncoder,
|
|
32
|
+
meshEncoder,
|
|
33
|
+
meshImporter,
|
|
34
|
+
imageDecoder,
|
|
35
|
+
audioDecoder,
|
|
36
|
+
aacEncoder,
|
|
37
|
+
onProgress: (id, msg) => {
|
|
38
|
+
parentPort.postMessage({ type: 'progress', id, msg });
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return ctx;
|
|
42
|
+
})();
|
|
43
|
+
return initPromise;
|
|
44
|
+
}
|
|
45
|
+
function harvestBlobs() {
|
|
46
|
+
const blobs = BlobStore.getAllProcessed();
|
|
47
|
+
const result = [];
|
|
48
|
+
for (const [key, data] of blobs) {
|
|
49
|
+
result.push([key, data]);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
parentPort.on('message', async (msg) => {
|
|
54
|
+
const { type, id, wasmDir } = msg;
|
|
55
|
+
try {
|
|
56
|
+
const processingCtx = await getCtx(wasmDir);
|
|
57
|
+
if (type === 'processAsset') {
|
|
58
|
+
const { sourceData, assetType, stringId, settings } = msg;
|
|
59
|
+
// Clear blobs from previous task
|
|
60
|
+
BlobStore.clearProcessed();
|
|
61
|
+
// Set source so pipeline can find it
|
|
62
|
+
BlobStore.setSource(id, sourceData);
|
|
63
|
+
const result = await processAsset(id, sourceData, assetType, stringId, settings, processingCtx);
|
|
64
|
+
const blobs = harvestBlobs();
|
|
65
|
+
parentPort.postMessage({ type: 'result', id, result, blobs });
|
|
66
|
+
}
|
|
67
|
+
else if (type === 'processExtractedMesh') {
|
|
68
|
+
const { childId, imported, hasSkeleton, stringId, settings } = msg;
|
|
69
|
+
BlobStore.clearProcessed();
|
|
70
|
+
const result = await processExtractedMesh(childId, imported, hasSkeleton, stringId, settings, processingCtx);
|
|
71
|
+
const blobs = harvestBlobs();
|
|
72
|
+
parentPort.postMessage({ type: 'result', id: childId, result, blobs });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const errorId = type === 'processExtractedMesh' ? msg.childId : id;
|
|
77
|
+
parentPort.postMessage({
|
|
78
|
+
type: 'error',
|
|
79
|
+
id: errorId,
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AssetType } from '../core/types.js';
|
|
2
|
+
import type { AssetSettings } from '../app/state.js';
|
|
3
|
+
import type { ProcessResult } from '../pipeline.js';
|
|
4
|
+
import type { ImportedMesh } from '../encoders/interfaces.js';
|
|
5
|
+
export declare class WorkerPool {
|
|
6
|
+
private workers;
|
|
7
|
+
private idle;
|
|
8
|
+
private pending;
|
|
9
|
+
private taskMap;
|
|
10
|
+
private wasmDir?;
|
|
11
|
+
private terminated;
|
|
12
|
+
constructor(opts?: {
|
|
13
|
+
poolSize?: number;
|
|
14
|
+
wasmDir?: string;
|
|
15
|
+
});
|
|
16
|
+
private handleMessage;
|
|
17
|
+
private handleError;
|
|
18
|
+
private releaseWorker;
|
|
19
|
+
private acquireWorker;
|
|
20
|
+
processAsset(task: {
|
|
21
|
+
id: string;
|
|
22
|
+
sourceData: Uint8Array;
|
|
23
|
+
type: AssetType;
|
|
24
|
+
stringId: string;
|
|
25
|
+
settings: AssetSettings;
|
|
26
|
+
}, onProgress?: (id: string, msg: string) => void): Promise<{
|
|
27
|
+
result: ProcessResult;
|
|
28
|
+
blobs: [string, Uint8Array][];
|
|
29
|
+
}>;
|
|
30
|
+
processExtractedMesh(task: {
|
|
31
|
+
childId: string;
|
|
32
|
+
imported: ImportedMesh;
|
|
33
|
+
hasSkeleton: boolean;
|
|
34
|
+
stringId: string;
|
|
35
|
+
settings: AssetSettings;
|
|
36
|
+
}, onProgress?: (id: string, msg: string) => void): Promise<{
|
|
37
|
+
result: ProcessResult;
|
|
38
|
+
blobs: [string, Uint8Array][];
|
|
39
|
+
}>;
|
|
40
|
+
terminate(): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
export class WorkerPool {
|
|
4
|
+
workers = [];
|
|
5
|
+
idle = [];
|
|
6
|
+
pending = [];
|
|
7
|
+
taskMap = new Map();
|
|
8
|
+
wasmDir;
|
|
9
|
+
terminated = false;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.wasmDir = opts?.wasmDir;
|
|
12
|
+
const poolSize = opts?.poolSize ?? Math.min(4, Math.max(1, os.cpus().length - 1));
|
|
13
|
+
for (let i = 0; i < poolSize; i++) {
|
|
14
|
+
const worker = new Worker(new URL('./process-worker.js', import.meta.url));
|
|
15
|
+
worker.on('message', (msg) => this.handleMessage(worker, msg));
|
|
16
|
+
worker.on('error', (err) => this.handleError(worker, err));
|
|
17
|
+
this.workers.push(worker);
|
|
18
|
+
this.idle.push(worker);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
handleMessage(worker, msg) {
|
|
22
|
+
const task = this.taskMap.get(worker);
|
|
23
|
+
if (!task)
|
|
24
|
+
return;
|
|
25
|
+
if (msg.type === 'progress') {
|
|
26
|
+
task.onProgress?.(msg.id, msg.msg);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (msg.type === 'result') {
|
|
30
|
+
this.taskMap.delete(worker);
|
|
31
|
+
this.releaseWorker(worker);
|
|
32
|
+
task.resolve({ result: msg.result, blobs: msg.blobs ?? [] });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (msg.type === 'error') {
|
|
36
|
+
this.taskMap.delete(worker);
|
|
37
|
+
this.releaseWorker(worker);
|
|
38
|
+
task.reject(new Error(msg.error ?? 'Unknown worker error'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
handleError(worker, err) {
|
|
43
|
+
const task = this.taskMap.get(worker);
|
|
44
|
+
if (task) {
|
|
45
|
+
this.taskMap.delete(worker);
|
|
46
|
+
task.reject(err);
|
|
47
|
+
}
|
|
48
|
+
// Replace dead worker
|
|
49
|
+
const idx = this.workers.indexOf(worker);
|
|
50
|
+
if (idx !== -1 && !this.terminated) {
|
|
51
|
+
const replacement = new Worker(new URL('./process-worker.js', import.meta.url));
|
|
52
|
+
replacement.on('message', (msg) => this.handleMessage(replacement, msg));
|
|
53
|
+
replacement.on('error', (e) => this.handleError(replacement, e));
|
|
54
|
+
this.workers[idx] = replacement;
|
|
55
|
+
this.releaseWorker(replacement);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
releaseWorker(worker) {
|
|
59
|
+
if (this.pending.length > 0) {
|
|
60
|
+
const next = this.pending.shift();
|
|
61
|
+
// Worker stays busy — the pending task will claim it
|
|
62
|
+
this.idle.push(worker);
|
|
63
|
+
next();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.idle.push(worker);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
acquireWorker() {
|
|
70
|
+
const worker = this.idle.pop();
|
|
71
|
+
if (worker)
|
|
72
|
+
return Promise.resolve(worker);
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
this.pending.push(() => {
|
|
75
|
+
const w = this.idle.pop();
|
|
76
|
+
resolve(w);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async processAsset(task, onProgress) {
|
|
81
|
+
if (this.terminated)
|
|
82
|
+
throw new Error('WorkerPool is terminated');
|
|
83
|
+
const worker = await this.acquireWorker();
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
this.taskMap.set(worker, { resolve, reject, onProgress });
|
|
86
|
+
const msg = {
|
|
87
|
+
type: 'processAsset',
|
|
88
|
+
id: task.id,
|
|
89
|
+
sourceData: task.sourceData,
|
|
90
|
+
assetType: task.type,
|
|
91
|
+
stringId: task.stringId,
|
|
92
|
+
settings: task.settings,
|
|
93
|
+
wasmDir: this.wasmDir,
|
|
94
|
+
};
|
|
95
|
+
worker.postMessage(msg);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async processExtractedMesh(task, onProgress) {
|
|
99
|
+
if (this.terminated)
|
|
100
|
+
throw new Error('WorkerPool is terminated');
|
|
101
|
+
const worker = await this.acquireWorker();
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.taskMap.set(worker, { resolve, reject, onProgress });
|
|
104
|
+
const msg = {
|
|
105
|
+
type: 'processExtractedMesh',
|
|
106
|
+
id: task.childId,
|
|
107
|
+
childId: task.childId,
|
|
108
|
+
imported: task.imported,
|
|
109
|
+
hasSkeleton: task.hasSkeleton,
|
|
110
|
+
stringId: task.stringId,
|
|
111
|
+
settings: task.settings,
|
|
112
|
+
wasmDir: this.wasmDir,
|
|
113
|
+
};
|
|
114
|
+
// Use structured clone (no transfer list) — mesh typed arrays may share
|
|
115
|
+
// underlying ArrayBuffers from the GLB binary chunk, making transfer unsafe.
|
|
116
|
+
worker.postMessage(msg);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async terminate() {
|
|
120
|
+
this.terminated = true;
|
|
121
|
+
// Reject any pending tasks
|
|
122
|
+
for (const cb of this.pending) {
|
|
123
|
+
// Tasks waiting for a worker — they'll never get one
|
|
124
|
+
}
|
|
125
|
+
this.pending = [];
|
|
126
|
+
await Promise.all(this.workers.map(w => w.terminate()));
|
|
127
|
+
this.workers = [];
|
|
128
|
+
this.idle = [];
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/stowkit-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
21
|
-
"@series-inc/stowkit-editor": "^0.1.
|
|
20
|
+
"@series-inc/stowkit-packer-gui": "^0.1.6",
|
|
21
|
+
"@series-inc/stowkit-editor": "^0.1.2",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|
|
24
24
|
"@strangeape/ffmpeg-audio-wasm": "^0.1.0",
|
package/skill.md
CHANGED
|
@@ -11,41 +11,54 @@ A StowKit project has a `.felicityproject` JSON file at its root:
|
|
|
11
11
|
"srcArtDir": "assets",
|
|
12
12
|
"name": "My Game",
|
|
13
13
|
"cdnAssetsPath": "public/cdn-assets",
|
|
14
|
+
"prefabsPath": "prefabs",
|
|
14
15
|
"packs": [{ "name": "default" }]
|
|
15
16
|
}
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
- `srcArtDir` — directory containing source art files (PNG, JPG, FBX, WAV, etc.)
|
|
19
|
+
- `srcArtDir` — directory containing source art files (PNG, JPG, FBX, GLB, WAV, etc.)
|
|
19
20
|
- `cdnAssetsPath` — output directory for built `.stow` packs
|
|
21
|
+
- `prefabsPath` — directory for prefab definitions (optional)
|
|
20
22
|
- `packs` — named packs to split assets into
|
|
21
23
|
|
|
22
24
|
## CLI Commands
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
npx stowkit init # Scaffold a new project (creates .felicityproject, assets/, public/cdn-assets/)
|
|
26
|
-
npx stowkit build # Full build: scan +
|
|
27
|
-
npx stowkit scan # Detect new assets and generate .stowmeta defaults
|
|
28
|
-
npx stowkit
|
|
29
|
-
npx stowkit
|
|
30
|
-
npx stowkit
|
|
27
|
+
npx stowkit init [dir] # Scaffold a new project (creates .felicityproject, assets/, public/cdn-assets/)
|
|
28
|
+
npx stowkit build [dir] # Full build: scan + process + pack
|
|
29
|
+
npx stowkit scan [dir] # Detect new assets and generate .stowmeta defaults
|
|
30
|
+
npx stowkit process [dir] # Compress assets (respects cache)
|
|
31
|
+
npx stowkit status [dir] # Show project summary, stale asset count
|
|
32
|
+
npx stowkit clean [dir] # Delete orphaned .stowcache and .stowmeta files
|
|
33
|
+
npx stowkit packer [dir] # Open the packer GUI in browser
|
|
34
|
+
npx stowkit editor [dir] # Open the level editor in browser
|
|
35
|
+
npx stowkit serve [dir] # Start API server only (no GUI)
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
All commands default to the current directory.
|
|
38
|
+
All commands default to the current directory.
|
|
39
|
+
|
|
40
|
+
**Options:**
|
|
41
|
+
- `--force` — Ignore cache and reprocess everything
|
|
42
|
+
- `--verbose` / `-v` — Detailed output
|
|
43
|
+
- `--port <number>` — Server port (default 3210)
|
|
34
44
|
|
|
35
45
|
## Supported Asset Types
|
|
36
46
|
|
|
37
47
|
| Type | Extensions | Compression |
|
|
38
48
|
|------|-----------|-------------|
|
|
49
|
+
| **GlbContainer** | **gltf, glb** | **Container — auto-extracts textures, meshes, materials, and animations** |
|
|
39
50
|
| Texture2D | png, jpg, jpeg, bmp, tga, webp, gif | KTX2 (Basis Universal) |
|
|
40
51
|
| Audio | wav, mp3, ogg, flac, aac, m4a | AAC (M4A container) |
|
|
41
|
-
| StaticMesh | fbx, obj
|
|
52
|
+
| StaticMesh | fbx, obj | Draco |
|
|
42
53
|
| SkinnedMesh | fbx | Uncompressed interleaved vertex data |
|
|
43
54
|
| AnimationClip | fbx | v2 format (Three.js-native tracks) |
|
|
44
55
|
| MaterialSchema | .stowmat | Metadata only (no data blob) |
|
|
45
56
|
|
|
57
|
+
**GLB/GLTF is the recommended format for 3D models.** Dropping a `.glb` file into the project is the easiest way to get meshes, textures, materials, and animations into the pipeline — everything is extracted and processed automatically. FBX and OBJ are still supported as standalone mesh formats but lack the automatic material/texture extraction that GLB provides.
|
|
58
|
+
|
|
46
59
|
## .stowmeta Files
|
|
47
60
|
|
|
48
|
-
Every source asset gets a `.stowmeta` sidecar file (JSON) that controls processing settings
|
|
61
|
+
Every source asset gets a `.stowmeta` sidecar file (JSON) that controls processing settings.
|
|
49
62
|
|
|
50
63
|
**Texture example:**
|
|
51
64
|
```json
|
|
@@ -119,6 +132,114 @@ Every source asset gets a `.stowmeta` sidecar file (JSON) that controls processi
|
|
|
119
132
|
}
|
|
120
133
|
```
|
|
121
134
|
|
|
135
|
+
## GLB Container Assets
|
|
136
|
+
|
|
137
|
+
When a `.glb` or `.gltf` file is added, it gets a `glbContainer` stowmeta. On the first build, the pipeline parses the GLB and automatically extracts all embedded content as **inline children** stored directly in the container's `.stowmeta`.
|
|
138
|
+
|
|
139
|
+
**GLB container stowmeta example:**
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"version": 1,
|
|
143
|
+
"type": "glbContainer",
|
|
144
|
+
"stringId": "hero",
|
|
145
|
+
"tags": [],
|
|
146
|
+
"pack": "default",
|
|
147
|
+
"preserveHierarchy": false,
|
|
148
|
+
"children": [
|
|
149
|
+
{
|
|
150
|
+
"name": "Hero_Diffuse.png",
|
|
151
|
+
"childType": "texture",
|
|
152
|
+
"stringId": "hero_diffuse",
|
|
153
|
+
"quality": "fastest",
|
|
154
|
+
"resize": "full",
|
|
155
|
+
"generateMipmaps": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "Hero",
|
|
159
|
+
"childType": "skinnedMesh",
|
|
160
|
+
"stringId": "hero",
|
|
161
|
+
"dracoQuality": "balanced",
|
|
162
|
+
"materialOverrides": {
|
|
163
|
+
"0": "models/hero.glb/HeroSkin.stowmat"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "HeroSkin.stowmat",
|
|
168
|
+
"childType": "materialSchema",
|
|
169
|
+
"stringId": "HeroSkin",
|
|
170
|
+
"materialConfig": {
|
|
171
|
+
"schemaId": "",
|
|
172
|
+
"properties": [
|
|
173
|
+
{
|
|
174
|
+
"fieldName": "BaseColor",
|
|
175
|
+
"fieldType": "texture",
|
|
176
|
+
"previewFlag": "mainTex",
|
|
177
|
+
"value": [1, 1, 1, 1],
|
|
178
|
+
"textureAsset": "models/hero.glb/Hero_Diffuse.png"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"fieldName": "Tint",
|
|
182
|
+
"fieldType": "color",
|
|
183
|
+
"previewFlag": "tint",
|
|
184
|
+
"value": [1, 1, 1, 1],
|
|
185
|
+
"textureAsset": null
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "Idle",
|
|
192
|
+
"childType": "animationClip",
|
|
193
|
+
"stringId": "idle",
|
|
194
|
+
"targetMeshId": null
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### GLB child types
|
|
201
|
+
|
|
202
|
+
Children extracted from a GLB container can be any of:
|
|
203
|
+
|
|
204
|
+
- **texture** — Embedded images (PNG/JPG) extracted from GLB binary chunk. Processed through the normal KTX2 pipeline.
|
|
205
|
+
- **staticMesh** — Meshes with no skeleton. Draco-compressed. Positions stored in local space when `preserveHierarchy` is true, otherwise baked to world space.
|
|
206
|
+
- **skinnedMesh** — Meshes with a skeleton and bone weights. Uncompressed interleaved vertex data.
|
|
207
|
+
- **animationClip** — Animations from the GLB. Processed into v2 format.
|
|
208
|
+
- **materialSchema** — PBR materials extracted from the GLB. Automatically converted from glTF PBR metallic-roughness to StowKit material configs with texture references pointing to sibling child textures.
|
|
209
|
+
|
|
210
|
+
### GLB child IDs
|
|
211
|
+
|
|
212
|
+
Child asset IDs follow the pattern `{containerId}/{childName}`. For example, if the container is `models/hero.glb`, its children have IDs like:
|
|
213
|
+
- `models/hero.glb/Hero_Diffuse.png` (texture)
|
|
214
|
+
- `models/hero.glb/Hero` (mesh)
|
|
215
|
+
- `models/hero.glb/HeroSkin.stowmat` (material)
|
|
216
|
+
- `models/hero.glb/Idle` (animation)
|
|
217
|
+
|
|
218
|
+
Material texture references within a GLB also use these child IDs (e.g. `"textureAsset": "models/hero.glb/Hero_Diffuse.png"`).
|
|
219
|
+
|
|
220
|
+
### GLB auto-assignment
|
|
221
|
+
|
|
222
|
+
The build pipeline automatically:
|
|
223
|
+
1. Parses the GLB and discovers all textures, meshes, materials, and animations
|
|
224
|
+
2. Creates a `children` manifest in the container's `.stowmeta`
|
|
225
|
+
3. Converts glTF PBR materials into `materialConfig` entries with correct texture references
|
|
226
|
+
4. Auto-assigns `materialOverrides` on mesh children based on GLB sub-mesh→material mappings
|
|
227
|
+
5. Processes each child through the appropriate encoder (KTX2 for textures, Draco for static meshes, etc.)
|
|
228
|
+
|
|
229
|
+
### preserveHierarchy
|
|
230
|
+
|
|
231
|
+
Set `"preserveHierarchy": true` on a GLB container to preserve the scene graph node hierarchy in extracted static meshes. When false (default), all mesh geometry is baked to world space and flattened. Skinned meshes are always excluded from hierarchy preservation.
|
|
232
|
+
|
|
233
|
+
### GLB child settings
|
|
234
|
+
|
|
235
|
+
Each child in the `children` array supports the same settings as its corresponding standalone asset type:
|
|
236
|
+
- Texture children: `quality`, `resize`, `generateMipmaps`
|
|
237
|
+
- Mesh children: `dracoQuality`, `materialOverrides`
|
|
238
|
+
- Animation children: `targetMeshId`
|
|
239
|
+
- All children: `excluded`, `tags`, `pack`, `stringId`
|
|
240
|
+
|
|
241
|
+
Edit these fields in the container's `.stowmeta` to customize per-child processing, then run `npx stowkit build`.
|
|
242
|
+
|
|
122
243
|
## .stowmat Files (Material Schemas)
|
|
123
244
|
|
|
124
245
|
Materials are defined as `.stowmat` JSON files placed in the source art directory:
|
|
@@ -150,6 +271,8 @@ Materials are defined as `.stowmat` JSON files placed in the source art director
|
|
|
150
271
|
**Preview flags:** none, mainTex, tint, alphaTest
|
|
151
272
|
**textureAsset:** relative path to a texture in the project (e.g. "textures/hero.png")
|
|
152
273
|
|
|
274
|
+
GLB containers also produce inline material children with a `materialConfig` field — these replace separate `.stowmat` files for GLB-extracted materials.
|
|
275
|
+
|
|
153
276
|
## Material Overrides on Meshes
|
|
154
277
|
|
|
155
278
|
To assign materials to mesh sub-meshes, set `materialOverrides` in the mesh's `.stowmeta`:
|
|
@@ -163,7 +286,9 @@ To assign materials to mesh sub-meshes, set `materialOverrides` in the mesh's `.
|
|
|
163
286
|
}
|
|
164
287
|
```
|
|
165
288
|
|
|
166
|
-
Keys are sub-mesh indices (as strings), values are relative paths to `.stowmat` files.
|
|
289
|
+
Keys are sub-mesh indices (as strings), values are relative paths to `.stowmat` files or GLB child material IDs (e.g. `"models/hero.glb/HeroSkin.stowmat"`).
|
|
290
|
+
|
|
291
|
+
For GLB mesh children, `materialOverrides` are auto-assigned from the GLB's sub-mesh→material mappings. You can override them by editing the child entry in the container's `.stowmeta`.
|
|
167
292
|
|
|
168
293
|
## Setting Up a New Project
|
|
169
294
|
|
|
@@ -178,6 +303,8 @@ Edit the `.stowmeta` file for any asset, then run `npx stowkit build`.
|
|
|
178
303
|
The build respects cache — only assets whose settings or source files changed get reprocessed.
|
|
179
304
|
Use `--force` to reprocess everything.
|
|
180
305
|
|
|
306
|
+
For GLB child assets, edit the child's entry inside the container's `.stowmeta` `children` array.
|
|
307
|
+
|
|
181
308
|
## Multi-Pack Setup
|
|
182
309
|
|
|
183
310
|
Split assets into multiple packs by editing `.felicityproject`:
|
|
@@ -193,19 +320,45 @@ Split assets into multiple packs by editing `.felicityproject`:
|
|
|
193
320
|
```
|
|
194
321
|
|
|
195
322
|
Then set `"pack": "level1"` in each asset's `.stowmeta` to assign it to a pack.
|
|
323
|
+
GLB children inherit the container's pack by default, but each child can override it with its own `"pack"` field.
|
|
196
324
|
|
|
197
325
|
## Cache
|
|
198
326
|
|
|
199
327
|
Processed assets are cached in `.stowcache` sidecar files next to the source.
|
|
200
328
|
The `.stowmeta` file stores a cache stamp (source size, modified time, settings hash).
|
|
201
329
|
Cache is automatically invalidated when source files or settings change.
|
|
330
|
+
GLB children have their own cache entries stored alongside the container.
|
|
202
331
|
Add `*.stowcache` to `.gitignore`.
|
|
203
332
|
|
|
204
333
|
## Common Tasks for AI Agents
|
|
205
334
|
|
|
335
|
+
### Adding a GLB model (recommended 3D workflow)
|
|
336
|
+
|
|
337
|
+
1. Place the `.glb` file into the `srcArtDir` (e.g. `assets/models/hero.glb`)
|
|
338
|
+
2. Run `npx stowkit build`
|
|
339
|
+
3. The pipeline automatically:
|
|
340
|
+
- Creates a `glbContainer` `.stowmeta` for the file
|
|
341
|
+
- Parses the GLB and extracts all textures, meshes, materials, and animations
|
|
342
|
+
- Populates the `children` array in the `.stowmeta` with one entry per extracted asset
|
|
343
|
+
- Converts glTF PBR materials to StowKit `materialConfig` with correct texture references
|
|
344
|
+
- Auto-assigns `materialOverrides` on mesh children from the GLB's sub-mesh→material mapping
|
|
345
|
+
- Processes each child (KTX2 for textures, Draco for static meshes, etc.)
|
|
346
|
+
4. To customize child settings after the first build, edit the `children` array entries in the container's `.stowmeta`
|
|
347
|
+
5. To exclude a child from packing, set `"excluded": true` on that child entry
|
|
348
|
+
6. To preserve the scene graph hierarchy in static meshes, set `"preserveHierarchy": true` on the container
|
|
349
|
+
|
|
350
|
+
### When to use preserveHierarchy
|
|
351
|
+
|
|
352
|
+
- **Default (false):** All mesh geometry is baked to world space and flattened into a single mesh. Use this for simple props and environment pieces.
|
|
353
|
+
- **true:** The original scene graph node hierarchy is preserved with local transforms (position, rotation, scale) per node. Use this for complex multi-part models where you need individual nodes at runtime (e.g. a vehicle with doors, a character with attachable accessories).
|
|
354
|
+
- Skinned meshes are always excluded from hierarchy preservation regardless of this setting.
|
|
355
|
+
|
|
356
|
+
### Other common tasks
|
|
357
|
+
|
|
206
358
|
- **Add a texture:** Drop a PNG/JPG into `assets/`, run `npx stowkit scan` to generate its `.stowmeta`, optionally edit settings, then `npx stowkit build`
|
|
207
359
|
- **Change compression quality:** Edit the `.stowmeta` file's quality/resize fields, then `npx stowkit build`
|
|
208
360
|
- **Create a material:** Create a `.stowmat` JSON file in `assets/`, run `npx stowkit scan`
|
|
209
361
|
- **Assign material to mesh:** Edit the mesh's `.stowmeta` to add `materialOverrides`
|
|
210
362
|
- **Check project health:** Run `npx stowkit status`
|
|
211
363
|
- **Full rebuild:** `npx stowkit build --force`
|
|
364
|
+
- **Clean orphaned files:** `npx stowkit clean`
|