@series-inc/stowkit-cli 0.1.12 → 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 +1 -1
- package/skill.md +164 -11
package/dist/orchestrator.js
CHANGED
|
@@ -5,16 +5,12 @@ import { cleanupProject } from './cleanup.js';
|
|
|
5
5
|
import { defaultAssetSettings } from './app/state.js';
|
|
6
6
|
import { BlobStore } from './app/blob-store.js';
|
|
7
7
|
import { readProjectConfig, scanDirectory, readFile, getFileSnapshot, } from './node-fs.js';
|
|
8
|
-
import { readStowmeta, writeStowmeta, stowmetaToAssetSettings, generateDefaultStowmeta, } from './app/stowmeta-io.js';
|
|
8
|
+
import { readStowmeta, writeStowmeta, stowmetaToAssetSettings, generateDefaultStowmeta, glbChildToAssetSettings, generateDefaultGlbChild, } from './app/stowmeta-io.js';
|
|
9
|
+
import { parseGlb, pbrToMaterialConfig } from './encoders/glb-loader.js';
|
|
9
10
|
import { readStowmat, stowmatToMaterialConfig } from './app/stowmat-io.js';
|
|
10
11
|
import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, } from './app/process-cache.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import { NodeBasisEncoder } from './encoders/basis-encoder.js';
|
|
14
|
-
import { NodeDracoEncoder } from './encoders/draco-encoder.js';
|
|
15
|
-
import { NodeAacEncoder, NodeAudioDecoder } from './encoders/aac-encoder.js';
|
|
16
|
-
import { NodeFbxImporter } from './encoders/fbx-loader.js';
|
|
17
|
-
import { SharpImageDecoder } from './encoders/image-decoder.js';
|
|
12
|
+
import { buildPack, processExtractedAnimations, validatePackDependencies } from './pipeline.js';
|
|
13
|
+
import { WorkerPool } from './workers/worker-pool.js';
|
|
18
14
|
export async function scanProject(projectDir, opts) {
|
|
19
15
|
const config = await readProjectConfig(projectDir);
|
|
20
16
|
const scan = await scanDirectory(config.srcArtDir);
|
|
@@ -102,6 +98,58 @@ export async function fullBuild(projectDir, opts) {
|
|
|
102
98
|
}
|
|
103
99
|
assets.push(asset);
|
|
104
100
|
assetsById.set(id, asset);
|
|
101
|
+
// Expand GlbContainer children
|
|
102
|
+
if (type === AssetType.GlbContainer && meta.type === 'glbContainer') {
|
|
103
|
+
const glbMeta = meta;
|
|
104
|
+
if (glbMeta.children && glbMeta.children.length > 0) {
|
|
105
|
+
asset.status = 'ready'; // Container itself is always "ready"
|
|
106
|
+
for (const child of glbMeta.children) {
|
|
107
|
+
const childId = `${id}/${child.name}`;
|
|
108
|
+
const baseName = child.name.replace(/\.[^.]+$/, '');
|
|
109
|
+
// Read settings from inline child entry
|
|
110
|
+
const { type: cType, settings: cSettings } = glbChildToAssetSettings(child);
|
|
111
|
+
const childAsset = {
|
|
112
|
+
id: childId,
|
|
113
|
+
fileName: child.name,
|
|
114
|
+
stringId: child.stringId || baseName,
|
|
115
|
+
type: cType,
|
|
116
|
+
status: 'pending',
|
|
117
|
+
settings: cSettings,
|
|
118
|
+
sourceSize: 0,
|
|
119
|
+
processedSize: 0,
|
|
120
|
+
parentId: id,
|
|
121
|
+
locked: true,
|
|
122
|
+
};
|
|
123
|
+
// Check cache
|
|
124
|
+
if (!force && child.cache) {
|
|
125
|
+
const cached = await readCacheBlobs(config.srcArtDir, childId);
|
|
126
|
+
if (cached) {
|
|
127
|
+
for (const [key, data] of cached) {
|
|
128
|
+
if (key === `${childId}:__metadata__`) {
|
|
129
|
+
try {
|
|
130
|
+
childAsset.metadata = JSON.parse(new TextDecoder().decode(data));
|
|
131
|
+
}
|
|
132
|
+
catch { /* skip */ }
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
BlobStore.setProcessed(key, data);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
childAsset.status = 'ready';
|
|
139
|
+
childAsset.processedSize = BlobStore.getProcessed(childId)?.length ?? 0;
|
|
140
|
+
if (verbose)
|
|
141
|
+
console.log(` [cached] ${childId}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
assets.push(childAsset);
|
|
145
|
+
assetsById.set(childId, childAsset);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// No children manifest yet — need to parse GLB to discover them
|
|
150
|
+
// This will be handled during processing
|
|
151
|
+
}
|
|
152
|
+
}
|
|
105
153
|
}
|
|
106
154
|
// Materials from .stowmat files
|
|
107
155
|
for (const matFile of scan.matFiles) {
|
|
@@ -130,37 +178,209 @@ export async function fullBuild(projectDir, opts) {
|
|
|
130
178
|
assets.push(asset);
|
|
131
179
|
assetsById.set(id, asset);
|
|
132
180
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
181
|
+
// 2b. Process GLB containers that need extraction (no children yet)
|
|
182
|
+
// Store extract results so mesh/animation children can be processed after encoder init
|
|
183
|
+
const glbExtracts = new Map();
|
|
184
|
+
const glbContainers = assets.filter(a => a.type === AssetType.GlbContainer && a.status === 'pending');
|
|
185
|
+
for (const container of glbContainers) {
|
|
186
|
+
try {
|
|
187
|
+
const sourceData = await readFile(config.srcArtDir, container.id);
|
|
188
|
+
if (!sourceData)
|
|
189
|
+
throw new Error(`Could not read source file: ${container.id}`);
|
|
190
|
+
// Build inline children with full settings
|
|
191
|
+
const containerMeta = (await readStowmeta(config.srcArtDir, container.id));
|
|
192
|
+
const preserveHierarchy = containerMeta.preserveHierarchy ?? false;
|
|
193
|
+
const extract = await parseGlb(sourceData, { preserveHierarchy });
|
|
194
|
+
glbExtracts.set(container.id, extract);
|
|
195
|
+
const existingChildren = new Map((containerMeta.children ?? []).map(c => [c.name, c]));
|
|
196
|
+
const childrenManifest = [];
|
|
197
|
+
for (const tex of extract.textures) {
|
|
198
|
+
childrenManifest.push(existingChildren.get(tex.name) ?? generateDefaultGlbChild(tex.name, 'texture'));
|
|
199
|
+
BlobStore.setSource(`${container.id}/${tex.name}`, tex.data);
|
|
200
|
+
}
|
|
201
|
+
for (const mesh of extract.meshes) {
|
|
202
|
+
const typeName = mesh.hasSkeleton ? 'skinnedMesh' : 'staticMesh';
|
|
203
|
+
childrenManifest.push(existingChildren.get(mesh.name) ?? generateDefaultGlbChild(mesh.name, typeName));
|
|
204
|
+
}
|
|
205
|
+
for (const mat of extract.materials) {
|
|
206
|
+
const matName = `${mat.name}.stowmat`;
|
|
207
|
+
const existing = existingChildren.get(matName);
|
|
208
|
+
if (existing) {
|
|
209
|
+
childrenManifest.push(existing);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const child = generateDefaultGlbChild(matName, 'materialSchema');
|
|
213
|
+
const matConfig = pbrToMaterialConfig(mat.pbrConfig, container.id);
|
|
214
|
+
child.materialConfig = {
|
|
215
|
+
schemaId: matConfig.schemaId,
|
|
216
|
+
properties: matConfig.properties.map(p => ({
|
|
217
|
+
fieldName: p.fieldName,
|
|
218
|
+
fieldType: typeof p.fieldType === 'number'
|
|
219
|
+
? (['texture', 'color', 'float', 'vec2', 'vec3', 'vec4', 'int'][p.fieldType] ?? 'color')
|
|
220
|
+
: p.fieldType,
|
|
221
|
+
previewFlag: typeof p.previewFlag === 'number'
|
|
222
|
+
? (['none', 'mainTex', 'tint', 'alphaTest'][p.previewFlag] ?? 'none')
|
|
223
|
+
: p.previewFlag,
|
|
224
|
+
value: [...p.value],
|
|
225
|
+
textureAsset: p.textureAssetId,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
childrenManifest.push(child);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const anim of extract.animations) {
|
|
232
|
+
childrenManifest.push(existingChildren.get(anim.name) ?? generateDefaultGlbChild(anim.name, 'animationClip'));
|
|
233
|
+
}
|
|
234
|
+
// Update stowmeta with inline children
|
|
235
|
+
containerMeta.children = childrenManifest;
|
|
236
|
+
await writeStowmeta(config.srcArtDir, container.id, containerMeta);
|
|
237
|
+
// Create child assets from inline entries
|
|
238
|
+
for (const child of childrenManifest) {
|
|
239
|
+
const childId = `${container.id}/${child.name}`;
|
|
240
|
+
if (assetsById.has(childId))
|
|
241
|
+
continue;
|
|
242
|
+
const baseName = child.name.replace(/\.[^.]+$/, '');
|
|
243
|
+
const { type: cType, settings: cSettings } = glbChildToAssetSettings(child);
|
|
244
|
+
const childAsset = {
|
|
245
|
+
id: childId,
|
|
246
|
+
fileName: child.name,
|
|
247
|
+
stringId: child.stringId || baseName,
|
|
248
|
+
type: cType,
|
|
249
|
+
status: 'pending',
|
|
250
|
+
settings: cSettings,
|
|
251
|
+
sourceSize: 0,
|
|
252
|
+
processedSize: 0,
|
|
253
|
+
parentId: container.id,
|
|
254
|
+
locked: true,
|
|
255
|
+
};
|
|
256
|
+
// Set up material configs for extracted materials
|
|
257
|
+
if (cType === AssetType.MaterialSchema) {
|
|
258
|
+
const matName = child.name.replace(/\.stowmat$/, '');
|
|
259
|
+
const mat = extract.materials.find(m => m.name === matName);
|
|
260
|
+
if (mat) {
|
|
261
|
+
if (!childAsset.settings.materialConfig?.properties?.length) {
|
|
262
|
+
childAsset.settings.materialConfig = pbrToMaterialConfig(mat.pbrConfig, container.id);
|
|
263
|
+
}
|
|
264
|
+
childAsset.status = 'ready';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Process animation children directly (no encoder needed)
|
|
268
|
+
if (cType === AssetType.AnimationClip) {
|
|
269
|
+
const anim = extract.animations.find(a => a.name === child.name);
|
|
270
|
+
if (anim) {
|
|
271
|
+
const result = processExtractedAnimations(childId, anim.clips, childAsset.stringId);
|
|
272
|
+
childAsset.status = 'ready';
|
|
273
|
+
childAsset.metadata = result.metadata;
|
|
274
|
+
childAsset.processedSize = result.processedSize;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
assets.push(childAsset);
|
|
278
|
+
assetsById.set(childId, childAsset);
|
|
279
|
+
}
|
|
280
|
+
// Auto-assign materialOverrides on mesh children from GLB sub-mesh→material mapping
|
|
281
|
+
for (const mesh of extract.meshes) {
|
|
282
|
+
const meshChildId = `${container.id}/${mesh.name}`;
|
|
283
|
+
const meshAsset = assetsById.get(meshChildId);
|
|
284
|
+
if (!meshAsset || Object.keys(meshAsset.settings.materialOverrides).length > 0)
|
|
285
|
+
continue;
|
|
286
|
+
const overrides = {};
|
|
287
|
+
for (let si = 0; si < mesh.imported.subMeshes.length; si++) {
|
|
288
|
+
const matIdx = mesh.imported.subMeshes[si].materialIndex;
|
|
289
|
+
const matName = mesh.imported.materials[matIdx]?.name;
|
|
290
|
+
if (matName) {
|
|
291
|
+
const matChildId = `${container.id}/${matName}.stowmat`;
|
|
292
|
+
if (assetsById.has(matChildId)) {
|
|
293
|
+
overrides[si] = matChildId;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (Object.keys(overrides).length > 0) {
|
|
298
|
+
meshAsset.settings.materialOverrides = overrides;
|
|
299
|
+
const inlineChild = childrenManifest.find(c => c.name === mesh.name);
|
|
300
|
+
if (inlineChild) {
|
|
301
|
+
inlineChild.materialOverrides = {};
|
|
302
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
303
|
+
inlineChild.materialOverrides[k] = v;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Persist updated stowmeta with auto-assigned materialOverrides
|
|
309
|
+
containerMeta.children = childrenManifest;
|
|
310
|
+
await writeStowmeta(config.srcArtDir, container.id, containerMeta);
|
|
311
|
+
container.status = 'ready';
|
|
312
|
+
if (verbose)
|
|
313
|
+
console.log(` [glb] ${container.id}: ${childrenManifest.length} children`);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
container.status = 'error';
|
|
317
|
+
container.error = err instanceof Error ? err.message : String(err);
|
|
318
|
+
console.error(` [glb] ${container.id} FAILED: ${container.error}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// 2c. Process GLB mesh children + stale assets via worker pool
|
|
322
|
+
const glbMeshChildren = assets.filter(a => a.status === 'pending' && a.parentId && (a.type === AssetType.StaticMesh || a.type === AssetType.SkinnedMesh));
|
|
323
|
+
const pending = assets.filter(a => a.status === 'pending' && !a.parentId);
|
|
324
|
+
const totalWork = glbMeshChildren.length + pending.length;
|
|
325
|
+
if (totalWork === 0) {
|
|
136
326
|
if (verbose)
|
|
137
327
|
console.log('All assets cached, nothing to process.');
|
|
138
328
|
}
|
|
139
329
|
else {
|
|
140
|
-
console.log(`Processing ${
|
|
141
|
-
|
|
142
|
-
const ctx = await initializeEncoders(opts?.wasmDir);
|
|
143
|
-
// Process with concurrency limit
|
|
330
|
+
console.log(`Processing ${totalWork} asset(s)...`);
|
|
331
|
+
const pool = new WorkerPool({ wasmDir: opts?.wasmDir });
|
|
144
332
|
let processed = 0;
|
|
333
|
+
// Process GLB mesh children
|
|
334
|
+
for (const child of glbMeshChildren) {
|
|
335
|
+
const extract = glbExtracts.get(child.parentId);
|
|
336
|
+
if (!extract)
|
|
337
|
+
continue;
|
|
338
|
+
const mesh = extract.meshes.find(m => `${child.parentId}/${m.name}` === child.id);
|
|
339
|
+
if (!mesh)
|
|
340
|
+
continue;
|
|
341
|
+
try {
|
|
342
|
+
const { result, blobs } = await pool.processExtractedMesh({ childId: child.id, imported: mesh.imported, hasSkeleton: mesh.hasSkeleton, stringId: child.stringId, settings: child.settings });
|
|
343
|
+
for (const [key, data] of blobs)
|
|
344
|
+
BlobStore.setProcessed(key, data);
|
|
345
|
+
child.status = 'ready';
|
|
346
|
+
child.metadata = result.metadata;
|
|
347
|
+
child.processedSize = result.processedSize;
|
|
348
|
+
processed++;
|
|
349
|
+
if (verbose)
|
|
350
|
+
console.log(` [${processed}/${totalWork}] ${child.id} (glb-mesh)`);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
child.status = 'error';
|
|
354
|
+
child.error = err instanceof Error ? err.message : String(err);
|
|
355
|
+
processed++;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Process remaining pending assets with concurrency limit
|
|
145
359
|
const queue = [...pending];
|
|
146
360
|
async function processNext() {
|
|
147
361
|
while (queue.length > 0) {
|
|
148
362
|
const asset = queue.shift();
|
|
149
363
|
const id = asset.id;
|
|
150
364
|
try {
|
|
151
|
-
// Load source
|
|
152
|
-
|
|
153
|
-
if (!sourceData)
|
|
154
|
-
|
|
365
|
+
// Load source (GLB texture children have source in BlobStore already)
|
|
366
|
+
let sourceData = BlobStore.getSource(id);
|
|
367
|
+
if (!sourceData) {
|
|
368
|
+
const data = await readFile(config.srcArtDir, id);
|
|
369
|
+
if (!data)
|
|
370
|
+
throw new Error(`Could not read source file: ${id}`);
|
|
371
|
+
sourceData = data;
|
|
372
|
+
}
|
|
155
373
|
BlobStore.setSource(id, sourceData);
|
|
156
374
|
const t0 = performance.now();
|
|
157
|
-
const result = await processAsset(id, sourceData, asset.type, asset.stringId, asset.settings
|
|
375
|
+
const { result, blobs } = await pool.processAsset({ id, sourceData, type: asset.type, stringId: asset.stringId, settings: asset.settings });
|
|
376
|
+
for (const [key, data] of blobs)
|
|
377
|
+
BlobStore.setProcessed(key, data);
|
|
158
378
|
const elapsed = (performance.now() - t0).toFixed(0);
|
|
159
379
|
asset.status = 'ready';
|
|
160
380
|
asset.metadata = result.metadata;
|
|
161
381
|
asset.processedSize = result.processedSize;
|
|
162
382
|
processed++;
|
|
163
|
-
console.log(` [${processed}/${
|
|
383
|
+
console.log(` [${processed}/${totalWork}] ${id} (${elapsed}ms)`);
|
|
164
384
|
// Write cache
|
|
165
385
|
const cacheEntries = new Map();
|
|
166
386
|
const processedBlob = BlobStore.getProcessed(id);
|
|
@@ -202,7 +422,7 @@ export async function fullBuild(projectDir, opts) {
|
|
|
202
422
|
asset.status = 'error';
|
|
203
423
|
asset.error = err instanceof Error ? err.message : String(err);
|
|
204
424
|
processed++;
|
|
205
|
-
console.error(` [${processed}/${
|
|
425
|
+
console.error(` [${processed}/${totalWork}] ${id} FAILED: ${asset.error}`);
|
|
206
426
|
}
|
|
207
427
|
}
|
|
208
428
|
}
|
|
@@ -211,6 +431,7 @@ export async function fullBuild(projectDir, opts) {
|
|
|
211
431
|
workers.push(processNext());
|
|
212
432
|
}
|
|
213
433
|
await Promise.all(workers);
|
|
434
|
+
await pool.terminate();
|
|
214
435
|
}
|
|
215
436
|
// 4. Clean orphaned caches/metas
|
|
216
437
|
await cleanupProject(config.projectDir, { verbose });
|
|
@@ -220,12 +441,22 @@ export async function fullBuild(projectDir, opts) {
|
|
|
220
441
|
await fs.mkdir(cdnDir, { recursive: true });
|
|
221
442
|
for (const pack of packs) {
|
|
222
443
|
const packAssets = assets.filter(a => a.status === 'ready' &&
|
|
444
|
+
a.type !== AssetType.GlbContainer &&
|
|
445
|
+
!a.settings.excluded &&
|
|
223
446
|
(a.settings.pack === pack.name || (!a.settings.pack && pack.name === 'default')));
|
|
224
447
|
if (packAssets.length === 0) {
|
|
225
448
|
if (verbose)
|
|
226
449
|
console.log(`Pack "${pack.name}": no assets, skipping`);
|
|
227
450
|
continue;
|
|
228
451
|
}
|
|
452
|
+
// Validate dependencies before building
|
|
453
|
+
const depErrors = validatePackDependencies(packAssets, assetsById);
|
|
454
|
+
if (depErrors.length > 0) {
|
|
455
|
+
console.error(`Pack "${pack.name}" has dependency errors:`);
|
|
456
|
+
for (const err of depErrors)
|
|
457
|
+
console.error(` ${err.assetId}: ${err.message}`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
229
460
|
console.log(`Building pack "${pack.name}" (${packAssets.length} assets)...`);
|
|
230
461
|
const packData = buildPack(packAssets, assetsById);
|
|
231
462
|
const outPath = path.join(cdnDir, `${pack.name}.stow`);
|
|
@@ -271,31 +502,3 @@ export async function showStatus(projectDir) {
|
|
|
271
502
|
}
|
|
272
503
|
console.log(`Cached: ${cached}, Needs processing: ${stale}`);
|
|
273
504
|
}
|
|
274
|
-
// ─── Initialize Encoders ────────────────────────────────────────────────────
|
|
275
|
-
async function initializeEncoders(wasmDir) {
|
|
276
|
-
const textureEncoder = new NodeBasisEncoder(wasmDir);
|
|
277
|
-
const meshEncoder = new NodeDracoEncoder();
|
|
278
|
-
const aacEncoder = new NodeAacEncoder();
|
|
279
|
-
const audioDecoder = new NodeAudioDecoder();
|
|
280
|
-
const meshImporter = new NodeFbxImporter();
|
|
281
|
-
const imageDecoder = new SharpImageDecoder();
|
|
282
|
-
await Promise.all([
|
|
283
|
-
textureEncoder.initialize(),
|
|
284
|
-
meshEncoder.initialize(),
|
|
285
|
-
aacEncoder.initialize(),
|
|
286
|
-
audioDecoder.initialize(),
|
|
287
|
-
]);
|
|
288
|
-
return {
|
|
289
|
-
textureEncoder,
|
|
290
|
-
meshEncoder,
|
|
291
|
-
meshImporter,
|
|
292
|
-
imageDecoder,
|
|
293
|
-
audioDecoder,
|
|
294
|
-
aacEncoder,
|
|
295
|
-
onProgress: (id, msg) => {
|
|
296
|
-
// Simple console progress
|
|
297
|
-
const name = id.split('/').pop() ?? id;
|
|
298
|
-
process.stdout.write(` [${name}] ${msg}\r`);
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssetType } from './core/types.js';
|
|
2
2
|
import type { AnimationClipMetadata } from './core/types.js';
|
|
3
3
|
import type { AssetSettings, ProjectAsset } from './app/state.js';
|
|
4
|
-
import type { ITextureEncoder, IMeshEncoder, IMeshImporter, IImageDecoder, IAudioDecoder, IAacEncoder, ImportedMesh } from './encoders/interfaces.js';
|
|
4
|
+
import type { ITextureEncoder, IMeshEncoder, IMeshImporter, IImageDecoder, IAudioDecoder, IAacEncoder, ImportedMesh, ImportedAnimation } from './encoders/interfaces.js';
|
|
5
|
+
import type { GlbExtractResult } from './encoders/glb-loader.js';
|
|
5
6
|
export interface ProcessingContext {
|
|
6
7
|
textureEncoder: ITextureEncoder;
|
|
7
8
|
meshEncoder: IMeshEncoder;
|
|
@@ -11,11 +12,29 @@ export interface ProcessingContext {
|
|
|
11
12
|
aacEncoder: IAacEncoder;
|
|
12
13
|
onProgress?: (id: string, msg: string) => void;
|
|
13
14
|
}
|
|
15
|
+
export interface GlbChildDescriptor {
|
|
16
|
+
name: string;
|
|
17
|
+
childType: string;
|
|
18
|
+
assetType: AssetType;
|
|
19
|
+
}
|
|
14
20
|
export interface ProcessResult {
|
|
15
21
|
metadata?: ProjectAsset['metadata'];
|
|
16
22
|
processedSize: number;
|
|
23
|
+
glbChildren?: GlbChildDescriptor[];
|
|
24
|
+
glbExtract?: GlbExtractResult;
|
|
17
25
|
}
|
|
18
26
|
export declare function processAsset(id: string, sourceData: Uint8Array, type: AssetType, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
27
|
+
export declare function processExtractedMesh(childId: string, imported: ImportedMesh, hasSkeleton: boolean, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
28
|
+
export declare function processExtractedAnimations(childId: string, clips: ImportedAnimation[], stringId: string): ProcessResult;
|
|
29
|
+
export interface DependencyError {
|
|
30
|
+
assetId: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate dependencies for a set of pack assets before building.
|
|
35
|
+
* Returns errors for missing/excluded/cross-pack texture and mesh references.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validatePackDependencies(packAssets: ProjectAsset[], allAssetsById: Map<string, ProjectAsset>): DependencyError[];
|
|
19
38
|
export declare function buildPack(assets: ProjectAsset[], assetsById: Map<string, ProjectAsset>): Uint8Array;
|
|
20
39
|
export declare function buildAnimationDataBlobsV2(imported: ImportedMesh): {
|
|
21
40
|
data: Uint8Array;
|
package/dist/pipeline.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AssetType, TextureChannelFormat, TextureResize, AudioSampleRate } from './core/types.js';
|
|
2
|
+
import { parseGlb } from './encoders/glb-loader.js';
|
|
2
3
|
import { dracoPresetToSettings } from './encoders/draco-encoder.js';
|
|
3
4
|
import { buildSkinnedMeshData } from './encoders/skinned-mesh-builder.js';
|
|
4
5
|
import { serializeTextureMetadata, serializeAudioMetadata, serializeMeshMetadata, serializeAnimationClipMetadata, serializeSkinnedMeshMetadata, serializeMaterialSchemaMetadata, deserializeSkinnedMeshMetadata, deserializeAnimationClipMetadata, } from './format/metadata.js';
|
|
@@ -59,7 +60,7 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
59
60
|
}
|
|
60
61
|
if (type === AssetType.StaticMesh) {
|
|
61
62
|
log('importing FBX...');
|
|
62
|
-
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
63
|
+
const imported = await ctx.meshImporter.import(sourceData, id, { preserveHierarchy: settings.preserveHierarchy });
|
|
63
64
|
const verts = imported.subMeshes.reduce((s, m) => s + m.positions.length / 3, 0);
|
|
64
65
|
log(`imported (${verts} verts, ${imported.subMeshes.length} submeshes)`);
|
|
65
66
|
const meshSettings = dracoPresetToSettings(settings.dracoQuality);
|
|
@@ -141,9 +142,140 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
141
142
|
log(`AAC encoded (${(aacData.length / 1024).toFixed(0)} KB)`);
|
|
142
143
|
return { metadata, processedSize: aacData.length };
|
|
143
144
|
}
|
|
145
|
+
if (type === AssetType.GlbContainer) {
|
|
146
|
+
log('parsing GLB...');
|
|
147
|
+
const extract = await parseGlb(sourceData, { preserveHierarchy: settings.preserveHierarchy });
|
|
148
|
+
const children = [];
|
|
149
|
+
for (const tex of extract.textures) {
|
|
150
|
+
children.push({ name: tex.name, childType: 'texture', assetType: AssetType.Texture2D });
|
|
151
|
+
}
|
|
152
|
+
for (const mesh of extract.meshes) {
|
|
153
|
+
const meshType = mesh.hasSkeleton ? AssetType.SkinnedMesh : AssetType.StaticMesh;
|
|
154
|
+
const typeName = mesh.hasSkeleton ? 'skinnedMesh' : 'staticMesh';
|
|
155
|
+
children.push({ name: mesh.name, childType: typeName, assetType: meshType });
|
|
156
|
+
}
|
|
157
|
+
for (const mat of extract.materials) {
|
|
158
|
+
children.push({ name: `${mat.name}.stowmat`, childType: 'materialSchema', assetType: AssetType.MaterialSchema });
|
|
159
|
+
}
|
|
160
|
+
for (const anim of extract.animations) {
|
|
161
|
+
children.push({ name: anim.name, childType: 'animationClip', assetType: AssetType.AnimationClip });
|
|
162
|
+
}
|
|
163
|
+
log(`extracted ${extract.textures.length} textures, ${extract.meshes.length} meshes, ${extract.materials.length} materials, ${extract.animations.length} animations`);
|
|
164
|
+
return { processedSize: 0, glbChildren: children, glbExtract: extract };
|
|
165
|
+
}
|
|
144
166
|
return { processedSize: 0 };
|
|
145
167
|
}
|
|
146
|
-
// ───
|
|
168
|
+
// ─── Process pre-extracted GLB child data ────────────────────────────────────
|
|
169
|
+
export async function processExtractedMesh(childId, imported, hasSkeleton, stringId, settings, ctx) {
|
|
170
|
+
if (hasSkeleton) {
|
|
171
|
+
const skinnedResult = buildSkinnedMeshData(imported, 0.01);
|
|
172
|
+
skinnedResult.metadata.stringId = stringId;
|
|
173
|
+
BlobStore.setProcessed(childId, skinnedResult.data);
|
|
174
|
+
BlobStore.setProcessed(`${childId}:skinnedMeta`, serializeSkinnedMeshMetadata(skinnedResult.metadata));
|
|
175
|
+
const animCount = imported.animations?.length ?? 0;
|
|
176
|
+
if (animCount > 0 && imported.animations) {
|
|
177
|
+
const animBlobs = buildAnimationDataBlobsV2(imported);
|
|
178
|
+
for (let ci = 0; ci < animBlobs.length; ci++) {
|
|
179
|
+
const ab = animBlobs[ci];
|
|
180
|
+
ab.metadata.stringId = animBlobs.length === 1 ? `${stringId}_anim` : `${stringId}_anim_${ci}`;
|
|
181
|
+
ab.metadata.targetMeshId = stringId;
|
|
182
|
+
BlobStore.setProcessed(`${childId}:anim:${ci}`, ab.data);
|
|
183
|
+
BlobStore.setProcessed(`${childId}:animMeta:${ci}`, serializeAnimationClipMetadata(ab.metadata));
|
|
184
|
+
}
|
|
185
|
+
BlobStore.setProcessed(`${childId}:animCount`, new Uint8Array([animBlobs.length]));
|
|
186
|
+
}
|
|
187
|
+
return { metadata: skinnedResult.metadata, processedSize: skinnedResult.data.length };
|
|
188
|
+
}
|
|
189
|
+
// Static mesh — encode with Draco
|
|
190
|
+
const meshSettings = dracoPresetToSettings(settings.dracoQuality);
|
|
191
|
+
const result = await ctx.meshEncoder.encode(imported, meshSettings);
|
|
192
|
+
result.metadata.stringId = stringId;
|
|
193
|
+
BlobStore.setProcessed(childId, result.data);
|
|
194
|
+
return { metadata: result.metadata, processedSize: result.data.length };
|
|
195
|
+
}
|
|
196
|
+
export function processExtractedAnimations(childId, clips, stringId) {
|
|
197
|
+
// Build a minimal ImportedMesh with just the animations
|
|
198
|
+
const dummyImported = {
|
|
199
|
+
subMeshes: [],
|
|
200
|
+
materials: [],
|
|
201
|
+
nodes: [],
|
|
202
|
+
hasSkeleton: false,
|
|
203
|
+
bones: [],
|
|
204
|
+
animations: clips,
|
|
205
|
+
};
|
|
206
|
+
const animBlobs = buildAnimationDataBlobsV2(dummyImported);
|
|
207
|
+
if (animBlobs.length === 0)
|
|
208
|
+
return { processedSize: 0 };
|
|
209
|
+
const primary = animBlobs[0];
|
|
210
|
+
primary.metadata.stringId = stringId;
|
|
211
|
+
BlobStore.setProcessed(childId, primary.data);
|
|
212
|
+
BlobStore.setProcessed(`${childId}:animMeta`, serializeAnimationClipMetadata(primary.metadata));
|
|
213
|
+
for (let ci = 1; ci < animBlobs.length; ci++) {
|
|
214
|
+
const ab = animBlobs[ci];
|
|
215
|
+
ab.metadata.stringId = `${stringId}_${ci}`;
|
|
216
|
+
BlobStore.setProcessed(`${childId}:anim:${ci}`, ab.data);
|
|
217
|
+
BlobStore.setProcessed(`${childId}:animMeta:${ci}`, serializeAnimationClipMetadata(ab.metadata));
|
|
218
|
+
}
|
|
219
|
+
BlobStore.setProcessed(`${childId}:animCount`, new Uint8Array([animBlobs.length]));
|
|
220
|
+
const totalBytes = animBlobs.reduce((s, a) => s + a.data.length, 0);
|
|
221
|
+
return { metadata: primary.metadata, processedSize: totalBytes };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Validate dependencies for a set of pack assets before building.
|
|
225
|
+
* Returns errors for missing/excluded/cross-pack texture and mesh references.
|
|
226
|
+
*/
|
|
227
|
+
export function validatePackDependencies(packAssets, allAssetsById) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
const packIds = new Set(packAssets.filter(a => !a.settings.excluded).map(a => a.id));
|
|
230
|
+
for (const asset of packAssets) {
|
|
231
|
+
if (asset.settings.excluded)
|
|
232
|
+
continue;
|
|
233
|
+
if (asset.type === AssetType.GlbContainer)
|
|
234
|
+
continue;
|
|
235
|
+
// MaterialSchema: check texture references
|
|
236
|
+
if (asset.type === AssetType.MaterialSchema) {
|
|
237
|
+
for (const prop of asset.settings.materialConfig.properties) {
|
|
238
|
+
if (!prop.textureAssetId)
|
|
239
|
+
continue;
|
|
240
|
+
const tex = allAssetsById.get(prop.textureAssetId);
|
|
241
|
+
if (!tex) {
|
|
242
|
+
errors.push({ assetId: asset.id, message: `Material "${asset.stringId}" references missing texture "${prop.textureAssetId}"` });
|
|
243
|
+
}
|
|
244
|
+
else if (tex.settings.excluded) {
|
|
245
|
+
errors.push({ assetId: asset.id, message: `Material "${asset.stringId}" references excluded texture "${tex.stringId}"` });
|
|
246
|
+
}
|
|
247
|
+
else if (!packIds.has(tex.id)) {
|
|
248
|
+
errors.push({ assetId: asset.id, message: `Material "${asset.stringId}" references texture "${tex.stringId}" which is not in the same pack` });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Mesh: check material override references
|
|
253
|
+
if (asset.type === AssetType.StaticMesh || asset.type === AssetType.SkinnedMesh) {
|
|
254
|
+
for (const [idx, matId] of Object.entries(asset.settings.materialOverrides)) {
|
|
255
|
+
if (!matId)
|
|
256
|
+
continue;
|
|
257
|
+
const mat = allAssetsById.get(matId);
|
|
258
|
+
if (!mat) {
|
|
259
|
+
errors.push({ assetId: asset.id, message: `Mesh "${asset.stringId}" sub-mesh ${idx} references missing material "${matId}"` });
|
|
260
|
+
}
|
|
261
|
+
else if (mat.settings.excluded) {
|
|
262
|
+
errors.push({ assetId: asset.id, message: `Mesh "${asset.stringId}" sub-mesh ${idx} references excluded material "${mat.stringId}"` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// AnimationClip: check target mesh reference
|
|
267
|
+
if (asset.type === AssetType.AnimationClip && asset.settings.targetMeshId) {
|
|
268
|
+
const target = allAssetsById.get(asset.settings.targetMeshId);
|
|
269
|
+
if (!target) {
|
|
270
|
+
errors.push({ assetId: asset.id, message: `Animation "${asset.stringId}" references missing target mesh "${asset.settings.targetMeshId}"` });
|
|
271
|
+
}
|
|
272
|
+
else if (target.settings.excluded) {
|
|
273
|
+
errors.push({ assetId: asset.id, message: `Animation "${asset.stringId}" references excluded target mesh "${target.stringId}"` });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return errors;
|
|
278
|
+
}
|
|
147
279
|
export function buildPack(assets, assetsById) {
|
|
148
280
|
const packer = new StowPacker();
|
|
149
281
|
const materialsByStringId = new Map(assets
|
|
@@ -152,6 +284,10 @@ export function buildPack(assets, assetsById) {
|
|
|
152
284
|
for (const asset of assets) {
|
|
153
285
|
if (asset.status !== 'ready')
|
|
154
286
|
continue;
|
|
287
|
+
if (asset.type === AssetType.GlbContainer)
|
|
288
|
+
continue;
|
|
289
|
+
if (asset.settings.excluded)
|
|
290
|
+
continue;
|
|
155
291
|
if (asset.type === AssetType.MaterialSchema) {
|
|
156
292
|
const schemaMeta = buildMaterialSchemaMetadata(asset, assetsById);
|
|
157
293
|
const schemaBytes = serializeMaterialSchemaMetadata(schemaMeta);
|