@series-inc/stowkit-cli 0.1.26 → 0.1.29
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/encoders/fbx-loader.js +3 -2
- package/dist/encoders/glb-loader.js +3 -2
- package/dist/encoders/interfaces.d.ts +7 -0
- package/dist/encoders/interfaces.js +107 -1
- package/dist/orchestrator.js +25 -2
- package/dist/pipeline.js +8 -2
- package/dist/server.js +129 -33
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
|
|
3
3
|
import { PropertyBinding } from 'three';
|
|
4
4
|
import { parseBinary as fbxParseBinary, parseText as fbxParseText, FBXReader } from 'fbx-parser';
|
|
5
|
+
import { mergeByMaterial, } from './interfaces.js';
|
|
5
6
|
// ─── Minimal DOM shim for Three.js FBXLoader in Node ────────────────────────
|
|
6
7
|
function ensureDomShim() {
|
|
7
8
|
const g = globalThis;
|
|
@@ -291,14 +292,14 @@ function extractMeshFromGroup(group) {
|
|
|
291
292
|
}
|
|
292
293
|
if (materials.length === 0)
|
|
293
294
|
materials.push({ name: 'default' });
|
|
294
|
-
return {
|
|
295
|
+
return mergeByMaterial({
|
|
295
296
|
subMeshes,
|
|
296
297
|
materials,
|
|
297
298
|
nodes,
|
|
298
299
|
hasSkeleton,
|
|
299
300
|
bones: allBones,
|
|
300
301
|
animations: [],
|
|
301
|
-
};
|
|
302
|
+
});
|
|
302
303
|
}
|
|
303
304
|
// ─── Hierarchical Group → ImportedMesh extraction ────────────────────────────
|
|
304
305
|
function extractMeshFromGroupHierarchical(group) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
3
|
+
import { mergeByMaterial } from './interfaces.js';
|
|
3
4
|
import { PropertyBinding } from 'three';
|
|
4
5
|
import { MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
5
6
|
// ─── DOM shim (shared pattern from fbx-loader) ─────────────────────────────
|
|
@@ -333,14 +334,14 @@ function extractMeshes(gltf) {
|
|
|
333
334
|
}
|
|
334
335
|
if (materials.length === 0)
|
|
335
336
|
materials.push({ name: 'default' });
|
|
336
|
-
const imported = {
|
|
337
|
+
const imported = mergeByMaterial({
|
|
337
338
|
subMeshes,
|
|
338
339
|
materials,
|
|
339
340
|
nodes,
|
|
340
341
|
hasSkeleton,
|
|
341
342
|
bones: allBones,
|
|
342
343
|
animations: [],
|
|
343
|
-
};
|
|
344
|
+
});
|
|
344
345
|
const meshName = meshObjects[0]?.name || 'mesh';
|
|
345
346
|
return [{ name: meshName, imported, hasSkeleton }];
|
|
346
347
|
}
|
|
@@ -83,6 +83,13 @@ export interface ImportedSkinData {
|
|
|
83
83
|
boneIndices: Uint32Array;
|
|
84
84
|
boneWeights: Float32Array;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
88
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
89
|
+
* Skinned submeshes are left unmerged.
|
|
90
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mergeByMaterial(mesh: ImportedMesh): ImportedMesh;
|
|
86
93
|
export interface DecodedPcm {
|
|
87
94
|
channels: Float32Array[];
|
|
88
95
|
sampleRate: number;
|
|
@@ -1 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
// ─── Mesh Merging (flat mode) ────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
4
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
5
|
+
* Skinned submeshes are left unmerged.
|
|
6
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
7
|
+
*/
|
|
8
|
+
export function mergeByMaterial(mesh) {
|
|
9
|
+
// Group non-skinned submeshes by materialIndex
|
|
10
|
+
const groups = new Map(); // materialIndex → submesh indices
|
|
11
|
+
const skinnedIndices = [];
|
|
12
|
+
for (let i = 0; i < mesh.subMeshes.length; i++) {
|
|
13
|
+
const sm = mesh.subMeshes[i];
|
|
14
|
+
if (sm.skinData) {
|
|
15
|
+
skinnedIndices.push(i);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
let list = groups.get(sm.materialIndex);
|
|
19
|
+
if (!list) {
|
|
20
|
+
list = [];
|
|
21
|
+
groups.set(sm.materialIndex, list);
|
|
22
|
+
}
|
|
23
|
+
list.push(i);
|
|
24
|
+
}
|
|
25
|
+
// Nothing to merge
|
|
26
|
+
if (groups.size === mesh.subMeshes.length - skinnedIndices.length && skinnedIndices.length === 0) {
|
|
27
|
+
// Already one submesh per material, just collapse to single node
|
|
28
|
+
return collapseToSingleNode(mesh);
|
|
29
|
+
}
|
|
30
|
+
const mergedSubMeshes = [];
|
|
31
|
+
// Merge each material group
|
|
32
|
+
for (const [matIdx, smIndices] of groups) {
|
|
33
|
+
if (smIndices.length === 1) {
|
|
34
|
+
mergedSubMeshes.push(mesh.subMeshes[smIndices[0]]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Calculate total sizes
|
|
38
|
+
let totalVerts = 0;
|
|
39
|
+
let totalIndices = 0;
|
|
40
|
+
let hasNormals = true;
|
|
41
|
+
let hasUvs = true;
|
|
42
|
+
for (const si of smIndices) {
|
|
43
|
+
const sm = mesh.subMeshes[si];
|
|
44
|
+
totalVerts += sm.positions.length / 3;
|
|
45
|
+
totalIndices += sm.indices.length;
|
|
46
|
+
if (!sm.normals)
|
|
47
|
+
hasNormals = false;
|
|
48
|
+
if (!sm.uvs)
|
|
49
|
+
hasUvs = false;
|
|
50
|
+
}
|
|
51
|
+
const positions = new Float32Array(totalVerts * 3);
|
|
52
|
+
const normals = hasNormals ? new Float32Array(totalVerts * 3) : null;
|
|
53
|
+
const uvs = hasUvs ? new Float32Array(totalVerts * 2) : null;
|
|
54
|
+
const indices = new Uint32Array(totalIndices);
|
|
55
|
+
let vertOffset = 0;
|
|
56
|
+
let idxOffset = 0;
|
|
57
|
+
for (const si of smIndices) {
|
|
58
|
+
const sm = mesh.subMeshes[si];
|
|
59
|
+
const vc = sm.positions.length / 3;
|
|
60
|
+
positions.set(sm.positions, vertOffset * 3);
|
|
61
|
+
if (normals && sm.normals)
|
|
62
|
+
normals.set(sm.normals, vertOffset * 3);
|
|
63
|
+
if (uvs && sm.uvs)
|
|
64
|
+
uvs.set(sm.uvs, vertOffset * 2);
|
|
65
|
+
// Offset indices to account for merged vertex buffer
|
|
66
|
+
for (let j = 0; j < sm.indices.length; j++) {
|
|
67
|
+
indices[idxOffset + j] = sm.indices[j] + vertOffset;
|
|
68
|
+
}
|
|
69
|
+
vertOffset += vc;
|
|
70
|
+
idxOffset += sm.indices.length;
|
|
71
|
+
}
|
|
72
|
+
mergedSubMeshes.push({ positions, normals, uvs, indices, materialIndex: matIdx });
|
|
73
|
+
}
|
|
74
|
+
// Append skinned submeshes unchanged
|
|
75
|
+
for (const si of skinnedIndices) {
|
|
76
|
+
mergedSubMeshes.push(mesh.subMeshes[si]);
|
|
77
|
+
}
|
|
78
|
+
// Single node referencing all submeshes — the user sees one mesh
|
|
79
|
+
const node = {
|
|
80
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
81
|
+
parentIndex: -1,
|
|
82
|
+
position: [0, 0, 0],
|
|
83
|
+
rotation: [0, 0, 0, 1],
|
|
84
|
+
scale: [1, 1, 1],
|
|
85
|
+
meshIndices: mergedSubMeshes.map((_, i) => i),
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
subMeshes: mergedSubMeshes,
|
|
89
|
+
materials: mesh.materials,
|
|
90
|
+
nodes: [node],
|
|
91
|
+
hasSkeleton: mesh.hasSkeleton,
|
|
92
|
+
bones: mesh.bones,
|
|
93
|
+
animations: mesh.animations,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Collapse N flat nodes into a single node referencing all submeshes */
|
|
97
|
+
function collapseToSingleNode(mesh) {
|
|
98
|
+
const node = {
|
|
99
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
100
|
+
parentIndex: -1,
|
|
101
|
+
position: [0, 0, 0],
|
|
102
|
+
rotation: [0, 0, 0, 1],
|
|
103
|
+
scale: [1, 1, 1],
|
|
104
|
+
meshIndices: mesh.subMeshes.map((_, i) => i),
|
|
105
|
+
};
|
|
106
|
+
return { ...mesh, nodes: [node] };
|
|
107
|
+
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -453,22 +453,45 @@ export async function showStatus(projectDir) {
|
|
|
453
453
|
console.log(`Meta files: ${scan.metaFiles.length}`);
|
|
454
454
|
const packs = config.config.packs ?? [{ name: 'default' }];
|
|
455
455
|
console.log(`Packs: ${packs.map(p => p.name).join(', ')}`);
|
|
456
|
-
// Count stale
|
|
456
|
+
// Count stale (including GLB children)
|
|
457
457
|
let cached = 0;
|
|
458
458
|
let stale = 0;
|
|
459
|
+
const staleIds = [];
|
|
459
460
|
for (const file of scan.sourceFiles) {
|
|
460
461
|
const meta = await readStowmeta(config.srcArtDir, file.relativePath);
|
|
461
462
|
if (!meta) {
|
|
462
463
|
stale++;
|
|
464
|
+
staleIds.push(file.relativePath);
|
|
463
465
|
continue;
|
|
464
466
|
}
|
|
465
467
|
const { type, settings } = stowmetaToAssetSettings(meta);
|
|
466
468
|
if (meta.cache && isCacheValid(meta, file, type, settings)) {
|
|
467
469
|
cached++;
|
|
468
470
|
}
|
|
469
|
-
else {
|
|
471
|
+
else if (type !== AssetType.GlbContainer) {
|
|
470
472
|
stale++;
|
|
473
|
+
staleIds.push(file.relativePath);
|
|
474
|
+
}
|
|
475
|
+
// Check GLB children
|
|
476
|
+
if (meta.type === 'glbContainer') {
|
|
477
|
+
const glbMeta = meta;
|
|
478
|
+
for (const child of glbMeta.children ?? []) {
|
|
479
|
+
if (child.childType === 'materialSchema')
|
|
480
|
+
continue;
|
|
481
|
+
const childId = `${file.relativePath}/${child.name}`;
|
|
482
|
+
if (child.cache) {
|
|
483
|
+
cached++;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
stale++;
|
|
487
|
+
staleIds.push(childId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
471
490
|
}
|
|
472
491
|
}
|
|
473
492
|
console.log(`Cached: ${cached}, Needs processing: ${stale}`);
|
|
493
|
+
if (staleIds.length > 0) {
|
|
494
|
+
for (const id of staleIds)
|
|
495
|
+
console.log(` ${id}`);
|
|
496
|
+
}
|
|
474
497
|
}
|
package/dist/pipeline.js
CHANGED
|
@@ -367,8 +367,14 @@ function resolveAssignedMaterial(meshAsset, subMeshIndex, sourceMaterialName, as
|
|
|
367
367
|
}
|
|
368
368
|
function toMaterialPropertyValue(prop, assetsById) {
|
|
369
369
|
const textureAsset = prop.textureAssetId ? assetsById.get(prop.textureAssetId) : undefined;
|
|
370
|
+
// Use previewFlag name as fallback when fieldName is empty (e.g. alphaTest)
|
|
371
|
+
const flagNames = {
|
|
372
|
+
1: 'mainTex', 2: 'tint', 3: 'alphaTest',
|
|
373
|
+
4: 'metalness', 5: 'roughness', 6: 'normalMap', 7: 'emissiveMap', 8: 'emissive',
|
|
374
|
+
};
|
|
375
|
+
const fieldName = prop.fieldName.trim() || flagNames[prop.previewFlag] || '';
|
|
370
376
|
return {
|
|
371
|
-
fieldName
|
|
377
|
+
fieldName,
|
|
372
378
|
value: [prop.value[0], prop.value[1], prop.value[2], prop.value[3]],
|
|
373
379
|
textureId: textureAsset?.stringId ?? '',
|
|
374
380
|
};
|
|
@@ -390,7 +396,7 @@ function applyMaterialAssignments(meshAsset, metadata, assetsById, materialsBySt
|
|
|
390
396
|
continue;
|
|
391
397
|
const config = assigned.settings.materialConfig;
|
|
392
398
|
const propertyValues = config.properties
|
|
393
|
-
.filter((prop) => prop.fieldName.trim().length > 0)
|
|
399
|
+
.filter((prop) => prop.fieldName.trim().length > 0 || prop.previewFlag > 0)
|
|
394
400
|
.map((prop) => toMaterialPropertyValue(prop, assetsById));
|
|
395
401
|
materials[i].schemaId = config.schemaId || assigned.stringId;
|
|
396
402
|
materials[i].properties = propertyValues;
|
package/dist/server.js
CHANGED
|
@@ -89,12 +89,18 @@ function queueProcessing(opts = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
// Build the processing queue
|
|
93
|
-
//
|
|
94
|
-
//
|
|
92
|
+
// Build the processing queue.
|
|
93
|
+
// If a GLB container is in the queue, skip its children — processGlbContainer
|
|
94
|
+
// handles all child processing internally. Without this, drain workers race:
|
|
95
|
+
// a child worker and the container worker both process the same children,
|
|
96
|
+
// and the container replaces the assets array mid-flight.
|
|
97
|
+
const containerIds = new Set(targets.filter(a => a.type === AssetType.GlbContainer).map(a => a.id));
|
|
95
98
|
const queue = [];
|
|
96
99
|
const seen = new Set();
|
|
97
100
|
for (const a of targets) {
|
|
101
|
+
// Skip children whose parent container is also being processed
|
|
102
|
+
if (a.parentId && containerIds.has(a.parentId))
|
|
103
|
+
continue;
|
|
98
104
|
if (!seen.has(a.id)) {
|
|
99
105
|
seen.add(a.id);
|
|
100
106
|
queue.push(a.id);
|
|
@@ -115,6 +121,16 @@ function queueProcessing(opts = {}) {
|
|
|
115
121
|
workers.push(drain());
|
|
116
122
|
}
|
|
117
123
|
await Promise.all(workers);
|
|
124
|
+
// Diagnostic: log any assets that aren't ready after processing completes
|
|
125
|
+
const notReady = assets.filter(a => a.status !== 'ready' && a.type !== AssetType.MaterialSchema);
|
|
126
|
+
if (notReady.length > 0) {
|
|
127
|
+
console.log(`[server] WARNING: ${notReady.length} asset(s) still not ready after processing:`);
|
|
128
|
+
for (const a of notReady)
|
|
129
|
+
console.log(` ${a.id} → ${a.status}${a.error ? ` (${a.error})` : ''}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(`[server] All ${assets.length} assets ready.`);
|
|
133
|
+
}
|
|
118
134
|
broadcast({ type: 'processing-complete' });
|
|
119
135
|
})();
|
|
120
136
|
if (opts.await)
|
|
@@ -155,7 +171,8 @@ async function expandGlbChildren(srcArtDir, containerId, containerMeta, configur
|
|
|
155
171
|
fileName: child.name,
|
|
156
172
|
stringId: child.stringId || baseName,
|
|
157
173
|
type: childType,
|
|
158
|
-
|
|
174
|
+
// Materials are metadata-only — always ready, no processing needed
|
|
175
|
+
status: childType === AssetType.MaterialSchema ? 'ready' : 'pending',
|
|
159
176
|
settings: childSettings,
|
|
160
177
|
sourceSize: 0,
|
|
161
178
|
processedSize: 0,
|
|
@@ -263,7 +280,14 @@ async function processGlbContainer(containerId) {
|
|
|
263
280
|
const existing = existingChildren.get(anim.name);
|
|
264
281
|
childrenManifest.push(existing ?? generateDefaultGlbChild(anim.name, 'animationClip'));
|
|
265
282
|
}
|
|
266
|
-
//
|
|
283
|
+
// Clear old cache stamps on all children — processGlbContainer always re-processes
|
|
284
|
+
// everything (the container was marked pending, so children must be reprocessed too).
|
|
285
|
+
// Without this, expandGlbChildren would restore children from stale cache and skip
|
|
286
|
+
// re-processing (e.g. after a preserveHierarchy toggle).
|
|
287
|
+
for (const child of childrenManifest) {
|
|
288
|
+
delete child.cache;
|
|
289
|
+
}
|
|
290
|
+
// Update container stowmeta with full inline children (cache stamps cleared)
|
|
267
291
|
if (containerStowmeta) {
|
|
268
292
|
containerStowmeta.children = childrenManifest;
|
|
269
293
|
await writeStowmeta(projectConfig.srcArtDir, containerId, containerStowmeta);
|
|
@@ -271,7 +295,7 @@ async function processGlbContainer(containerId) {
|
|
|
271
295
|
// Remove old child assets from the list
|
|
272
296
|
const prefix = containerId + '/';
|
|
273
297
|
assets = assets.filter(a => !a.id.startsWith(prefix));
|
|
274
|
-
// Create child assets from inline entries
|
|
298
|
+
// Create child assets from inline entries (all will be 'pending' since cache was cleared)
|
|
275
299
|
const childAssets = await expandGlbChildren(projectConfig.srcArtDir, containerId, { ...containerStowmeta, children: childrenManifest }, configuredPacks);
|
|
276
300
|
// Set up material configs for extracted GLB materials (runtime config from inline)
|
|
277
301
|
for (const mat of extract.materials) {
|
|
@@ -352,16 +376,56 @@ async function processGlbContainer(containerId) {
|
|
|
352
376
|
}
|
|
353
377
|
}
|
|
354
378
|
}
|
|
355
|
-
//
|
|
379
|
+
// Process texture children (their raw image data is in BlobStore from extraction)
|
|
380
|
+
const pendingTextures = childAssets.filter(a => a.status === 'pending' && a.type === AssetType.Texture2D);
|
|
381
|
+
await Promise.all(pendingTextures.map(child => processOneAsset(child.id)));
|
|
382
|
+
// Write cache blobs and stamps for ALL children processed in this container.
|
|
383
|
+
// Without this, a server restart / rescan would see no cache → children restart as 'pending'.
|
|
384
|
+
const parentSnapshot = await getFileSnapshot(projectConfig.srcArtDir, containerId);
|
|
385
|
+
for (const childAsset of childAssets) {
|
|
386
|
+
if (childAsset.status !== 'ready')
|
|
387
|
+
continue;
|
|
388
|
+
if (childAsset.type === AssetType.MaterialSchema)
|
|
389
|
+
continue;
|
|
390
|
+
const childName = childAsset.id.split('/').pop();
|
|
391
|
+
const inlineChild = childrenManifest.find(c => c.name === childName);
|
|
392
|
+
if (!inlineChild)
|
|
393
|
+
continue;
|
|
394
|
+
// Collect cache blobs for this child
|
|
395
|
+
const cacheEntries = new Map();
|
|
396
|
+
const processed = BlobStore.getProcessed(childAsset.id);
|
|
397
|
+
if (processed)
|
|
398
|
+
cacheEntries.set(childAsset.id, processed);
|
|
399
|
+
if (childAsset.metadata) {
|
|
400
|
+
cacheEntries.set(`${childAsset.id}:__metadata__`, new TextEncoder().encode(JSON.stringify(childAsset.metadata)));
|
|
401
|
+
}
|
|
402
|
+
for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
|
|
403
|
+
const blob = BlobStore.getProcessed(`${childAsset.id}${suffix}`);
|
|
404
|
+
if (blob)
|
|
405
|
+
cacheEntries.set(`${childAsset.id}${suffix}`, blob);
|
|
406
|
+
}
|
|
407
|
+
const animCountBlob = BlobStore.getProcessed(`${childAsset.id}:animCount`);
|
|
408
|
+
const clipCount = animCountBlob ? animCountBlob[0] : 0;
|
|
409
|
+
for (let ci = 0; ci < clipCount; ci++) {
|
|
410
|
+
for (const key of [`${childAsset.id}:anim:${ci}`, `${childAsset.id}:animMeta:${ci}`]) {
|
|
411
|
+
const blob = BlobStore.getProcessed(key);
|
|
412
|
+
if (blob)
|
|
413
|
+
cacheEntries.set(key, blob);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (cacheEntries.size > 0) {
|
|
417
|
+
await writeCacheBlobs(projectConfig.srcArtDir, childAsset.id, cacheEntries);
|
|
418
|
+
}
|
|
419
|
+
// Set cache stamp on the inline child entry (uses parent file snapshot)
|
|
420
|
+
if (parentSnapshot) {
|
|
421
|
+
inlineChild.cache = buildCacheStamp(parentSnapshot, childAsset.type, childAsset.settings);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Persist updated stowmeta with materialOverrides AND cache stamps
|
|
356
425
|
if (containerStowmeta) {
|
|
357
426
|
containerStowmeta.children = childrenManifest;
|
|
358
427
|
await writeStowmeta(projectConfig.srcArtDir, containerId, containerStowmeta);
|
|
359
428
|
}
|
|
360
|
-
// Queue texture children for processing (their raw image data is in BlobStore)
|
|
361
|
-
const pendingTextures = childAssets.filter(a => a.status === 'pending' && a.type === AssetType.Texture2D);
|
|
362
|
-
for (const child of pendingTextures) {
|
|
363
|
-
processOneAsset(child.id);
|
|
364
|
-
}
|
|
365
429
|
}
|
|
366
430
|
async function readBody(req) {
|
|
367
431
|
const chunks = [];
|
|
@@ -528,26 +592,21 @@ async function processOneAsset(id) {
|
|
|
528
592
|
throw new Error(`Could not read parent GLB: ${asset.parentId}`);
|
|
529
593
|
parentSource = data;
|
|
530
594
|
}
|
|
531
|
-
|
|
595
|
+
// Read parent's preserveHierarchy setting
|
|
596
|
+
const parentMeta = await readStowmeta(projectConfig.srcArtDir, asset.parentId);
|
|
597
|
+
const preserveHierarchy = parentMeta?.preserveHierarchy ?? false;
|
|
598
|
+
const extract = await parseGlb(parentSource, { preserveHierarchy });
|
|
532
599
|
const childName = id.split('/').pop();
|
|
600
|
+
let childResult = null;
|
|
533
601
|
if (asset.type === AssetType.Texture2D) {
|
|
534
602
|
const tex = extract.textures.find(t => t.name === childName);
|
|
535
603
|
if (!tex)
|
|
536
604
|
throw new Error(`Texture "${childName}" not found in GLB`);
|
|
537
605
|
BlobStore.setSource(id, tex.data);
|
|
538
|
-
// Process via worker pool
|
|
539
606
|
const { result, blobs } = await workerPool.processAsset({ id, sourceData: tex.data, type: asset.type, stringId: asset.stringId, settings: asset.settings }, (pid, msg) => broadcast({ type: 'progress', id: pid, message: msg }));
|
|
540
607
|
for (const [key, data] of blobs)
|
|
541
608
|
BlobStore.setProcessed(key, data);
|
|
542
|
-
|
|
543
|
-
asset.status = 'ready';
|
|
544
|
-
asset.metadata = result.metadata;
|
|
545
|
-
asset.processedSize = result.processedSize;
|
|
546
|
-
broadcast({
|
|
547
|
-
type: 'asset-update', id,
|
|
548
|
-
updates: { status: 'ready', metadata: result.metadata, processedSize: result.processedSize },
|
|
549
|
-
});
|
|
550
|
-
}
|
|
609
|
+
childResult = result;
|
|
551
610
|
}
|
|
552
611
|
else if (asset.type === AssetType.StaticMesh || asset.type === AssetType.SkinnedMesh) {
|
|
553
612
|
const mesh = extract.meshes.find(m => m.name === childName);
|
|
@@ -556,15 +615,7 @@ async function processOneAsset(id) {
|
|
|
556
615
|
const { result, blobs } = await workerPool.processExtractedMesh({ childId: id, imported: mesh.imported, hasSkeleton: mesh.hasSkeleton, stringId: asset.stringId, settings: asset.settings }, (pid, msg) => broadcast({ type: 'progress', id: pid, message: msg }));
|
|
557
616
|
for (const [key, data] of blobs)
|
|
558
617
|
BlobStore.setProcessed(key, data);
|
|
559
|
-
|
|
560
|
-
asset.status = 'ready';
|
|
561
|
-
asset.metadata = result.metadata;
|
|
562
|
-
asset.processedSize = result.processedSize;
|
|
563
|
-
broadcast({
|
|
564
|
-
type: 'asset-update', id,
|
|
565
|
-
updates: { status: 'ready', metadata: result.metadata, processedSize: result.processedSize },
|
|
566
|
-
});
|
|
567
|
-
}
|
|
618
|
+
childResult = result;
|
|
568
619
|
}
|
|
569
620
|
else {
|
|
570
621
|
// Animation or other — fall back to full container reprocess
|
|
@@ -575,7 +626,52 @@ async function processOneAsset(id) {
|
|
|
575
626
|
}
|
|
576
627
|
return;
|
|
577
628
|
}
|
|
578
|
-
|
|
629
|
+
if (childResult && asset.status !== 'pending') {
|
|
630
|
+
asset.status = 'ready';
|
|
631
|
+
asset.metadata = childResult.metadata;
|
|
632
|
+
asset.processedSize = childResult.processedSize;
|
|
633
|
+
broadcast({
|
|
634
|
+
type: 'asset-update', id,
|
|
635
|
+
updates: { status: 'ready', metadata: childResult.metadata, processedSize: childResult.processedSize },
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
// Write cache for this child
|
|
639
|
+
const cacheEntries = new Map();
|
|
640
|
+
const processed = BlobStore.getProcessed(id);
|
|
641
|
+
if (processed)
|
|
642
|
+
cacheEntries.set(id, processed);
|
|
643
|
+
if (childResult?.metadata) {
|
|
644
|
+
cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(childResult.metadata)));
|
|
645
|
+
}
|
|
646
|
+
for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
|
|
647
|
+
const blob = BlobStore.getProcessed(`${id}${suffix}`);
|
|
648
|
+
if (blob)
|
|
649
|
+
cacheEntries.set(`${id}${suffix}`, blob);
|
|
650
|
+
}
|
|
651
|
+
const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
|
|
652
|
+
const clipCount = animCountBlob ? animCountBlob[0] : 0;
|
|
653
|
+
for (let ci = 0; ci < clipCount; ci++) {
|
|
654
|
+
for (const key of [`${id}:anim:${ci}`, `${id}:animMeta:${ci}`]) {
|
|
655
|
+
const blob = BlobStore.getProcessed(key);
|
|
656
|
+
if (blob)
|
|
657
|
+
cacheEntries.set(key, blob);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (cacheEntries.size > 0) {
|
|
661
|
+
await writeCacheBlobs(projectConfig.srcArtDir, id, cacheEntries);
|
|
662
|
+
// Update cache stamp in parent's stowmeta for this child
|
|
663
|
+
if (parentMeta) {
|
|
664
|
+
const inlineChild = parentMeta.children.find(c => c.name === childName);
|
|
665
|
+
if (inlineChild) {
|
|
666
|
+
const parentSnapshot = await getFileSnapshot(projectConfig.srcArtDir, asset.parentId);
|
|
667
|
+
if (parentSnapshot) {
|
|
668
|
+
inlineChild.cache = buildCacheStamp(parentSnapshot, asset.type, asset.settings);
|
|
669
|
+
await writeStowmeta(projectConfig.srcArtDir, asset.parentId, parentMeta);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
console.log(`[server] Re-processed GLB child ${id} (status: ${asset.status}, in assets: ${assets.some(a => a.id === id)})`);
|
|
579
675
|
}
|
|
580
676
|
catch (err) {
|
|
581
677
|
asset.status = 'error';
|
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.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
20
|
+
"@series-inc/stowkit-packer-gui": "^0.1.16",
|
|
21
21
|
"@series-inc/stowkit-editor": "^0.1.2",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|