@series-inc/stowkit-cli 0.1.25 → 0.1.27
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/cli.js +11 -0
- package/dist/inspect.d.ts +3 -0
- package/dist/inspect.js +165 -0
- package/dist/orchestrator.js +25 -2
- package/dist/server.js +135 -37
- package/package.json +1 -1
- package/skill.md +1 -0
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { initProject } from './init.js';
|
|
|
9
9
|
import { cleanupProject } from './cleanup.js';
|
|
10
10
|
import { createMaterial } from './create-material.js';
|
|
11
11
|
import { renameAsset, moveAsset, deleteAsset, setStringId } from './asset-commands.js';
|
|
12
|
+
import { inspectPack } from './inspect.js';
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
function checkForUpdate() {
|
|
@@ -49,6 +50,7 @@ Usage:
|
|
|
49
50
|
stowkit move <path> <folder> Move an asset to a different folder
|
|
50
51
|
stowkit delete <path> Delete an asset and its sidecar files
|
|
51
52
|
stowkit set-id <path> <id> Change an asset's stringId
|
|
53
|
+
stowkit inspect <file.stow> Show manifest of a built .stow pack
|
|
52
54
|
stowkit update Update CLI to latest version and refresh skill files
|
|
53
55
|
stowkit version Show installed version
|
|
54
56
|
stowkit packer [dir] Open the packer GUI
|
|
@@ -208,6 +210,15 @@ async function main() {
|
|
|
208
210
|
await setStringId('.', positional[0], positional[1]);
|
|
209
211
|
break;
|
|
210
212
|
}
|
|
213
|
+
case 'inspect': {
|
|
214
|
+
const stowPath = args.find(a => !a.startsWith('-') && a !== command);
|
|
215
|
+
if (!stowPath) {
|
|
216
|
+
console.error('Usage: stowkit inspect <file.stow>');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
await inspectPack(stowPath, { verbose });
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
211
222
|
case 'packer': {
|
|
212
223
|
const packerDir = resolveAppDir('@series-inc/stowkit-packer-gui', 'stowkit-packer-gui');
|
|
213
224
|
if (!packerDir) {
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { BinaryReader } from './core/binary.js';
|
|
4
|
+
import { AssetType } from './core/types.js';
|
|
5
|
+
import { STOW_MAGIC, FILE_HEADER_SIZE, STRING_ID_SIZE, } from './core/constants.js';
|
|
6
|
+
import { unwrapMetadata } from './format/metadata.js';
|
|
7
|
+
const ASSET_TYPE_NAMES = {
|
|
8
|
+
[AssetType.Unknown]: 'unknown',
|
|
9
|
+
[AssetType.StaticMesh]: 'staticMesh',
|
|
10
|
+
[AssetType.Texture2D]: 'texture',
|
|
11
|
+
[AssetType.Audio]: 'audio',
|
|
12
|
+
[AssetType.MaterialSchema]: 'material',
|
|
13
|
+
[AssetType.SkinnedMesh]: 'skinnedMesh',
|
|
14
|
+
[AssetType.AnimationClip]: 'animation',
|
|
15
|
+
[AssetType.GlbContainer]: 'glbContainer',
|
|
16
|
+
};
|
|
17
|
+
function formatBytes(bytes) {
|
|
18
|
+
if (bytes < 1024)
|
|
19
|
+
return `${bytes} B`;
|
|
20
|
+
if (bytes < 1024 * 1024)
|
|
21
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
22
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
23
|
+
}
|
|
24
|
+
/** Read stringId from the start of unwrapped asset metadata based on type. */
|
|
25
|
+
function readStringIdAndDetails(type, meta) {
|
|
26
|
+
const r = new BinaryReader(meta);
|
|
27
|
+
switch (type) {
|
|
28
|
+
case AssetType.Texture2D: {
|
|
29
|
+
const width = r.readUint32();
|
|
30
|
+
const height = r.readUint32();
|
|
31
|
+
const channels = r.readUint32();
|
|
32
|
+
r.readUint32(); // channelFormat
|
|
33
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
34
|
+
return { stringId, details: `${width}x${height}, ${channels}ch` };
|
|
35
|
+
}
|
|
36
|
+
case AssetType.Audio: {
|
|
37
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
38
|
+
const sampleRate = r.readUint32();
|
|
39
|
+
const channels = r.readUint32();
|
|
40
|
+
const durationMs = r.readUint32();
|
|
41
|
+
const durSec = (durationMs / 1000).toFixed(1);
|
|
42
|
+
return { stringId, details: `${durSec}s, ${sampleRate}Hz, ${channels}ch` };
|
|
43
|
+
}
|
|
44
|
+
case AssetType.StaticMesh: {
|
|
45
|
+
const geoCount = r.readUint32();
|
|
46
|
+
const matCount = r.readUint32();
|
|
47
|
+
const nodeCount = r.readUint32();
|
|
48
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
49
|
+
return { stringId, details: `${geoCount} geo, ${matCount} mat, ${nodeCount} nodes` };
|
|
50
|
+
}
|
|
51
|
+
case AssetType.SkinnedMesh: {
|
|
52
|
+
const geoCount = r.readUint32();
|
|
53
|
+
const matCount = r.readUint32();
|
|
54
|
+
const nodeCount = r.readUint32();
|
|
55
|
+
const boneCount = r.readUint32();
|
|
56
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
57
|
+
return { stringId, details: `${geoCount} geo, ${boneCount} bones` };
|
|
58
|
+
}
|
|
59
|
+
case AssetType.AnimationClip: {
|
|
60
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
61
|
+
r.readFixedString(STRING_ID_SIZE); // targetMeshId
|
|
62
|
+
r.readUint32(); // metadataVersion
|
|
63
|
+
const duration = r.readFloat32();
|
|
64
|
+
const trackCount = r.readUint32();
|
|
65
|
+
return { stringId, details: `${duration.toFixed(2)}s, ${trackCount} tracks` };
|
|
66
|
+
}
|
|
67
|
+
case AssetType.MaterialSchema: {
|
|
68
|
+
const stringId = r.readFixedString(STRING_ID_SIZE);
|
|
69
|
+
const schemaName = r.readFixedString(64);
|
|
70
|
+
const fieldCount = r.readUint32();
|
|
71
|
+
return { stringId, details: `schema: ${schemaName}, ${fieldCount} fields` };
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return { stringId: '?', details: '' };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function inspectPack(filePath, opts) {
|
|
78
|
+
const absPath = path.resolve(filePath);
|
|
79
|
+
const data = await fs.readFile(absPath);
|
|
80
|
+
const buf = new Uint8Array(data);
|
|
81
|
+
if (buf.length < FILE_HEADER_SIZE) {
|
|
82
|
+
throw new Error(`File too small to be a .stow pack: ${buf.length} bytes`);
|
|
83
|
+
}
|
|
84
|
+
// Read header
|
|
85
|
+
const hr = new BinaryReader(buf);
|
|
86
|
+
const magic = hr.readUint32();
|
|
87
|
+
if (magic !== STOW_MAGIC) {
|
|
88
|
+
throw new Error(`Invalid magic: 0x${magic.toString(16)} (expected 0x${STOW_MAGIC.toString(16)})`);
|
|
89
|
+
}
|
|
90
|
+
const version = hr.readUint32();
|
|
91
|
+
const assetCount = hr.readUint32();
|
|
92
|
+
const dirOffset = hr.readUint64AsNumber();
|
|
93
|
+
// Read directory entries
|
|
94
|
+
const entries = [];
|
|
95
|
+
const dr = new BinaryReader(buf, dirOffset);
|
|
96
|
+
for (let i = 0; i < assetCount; i++) {
|
|
97
|
+
entries.push({
|
|
98
|
+
uid: dr.readUint64(),
|
|
99
|
+
type: dr.readUint32(),
|
|
100
|
+
dataOffset: dr.readUint64AsNumber(),
|
|
101
|
+
dataSize: dr.readUint64AsNumber(),
|
|
102
|
+
metadataOffset: dr.readUint64AsNumber(),
|
|
103
|
+
metadataSize: dr.readUint32(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Parse each asset's metadata
|
|
107
|
+
const assets = [];
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const typeName = ASSET_TYPE_NAMES[entry.type] ?? `type(${entry.type})`;
|
|
110
|
+
let stringId = '?';
|
|
111
|
+
let tags = [];
|
|
112
|
+
let details = '';
|
|
113
|
+
if (entry.metadataOffset > 0 && entry.metadataSize > 0) {
|
|
114
|
+
const rawMeta = buf.slice(entry.metadataOffset, entry.metadataOffset + entry.metadataSize);
|
|
115
|
+
// Try tag-wrapped first; if tagCsvLength looks unreasonable, treat as raw metadata
|
|
116
|
+
let assetMeta;
|
|
117
|
+
try {
|
|
118
|
+
const unwrapped = unwrapMetadata(rawMeta);
|
|
119
|
+
// Sanity check: tagCsvLength should be small (< 4096); if it's huge, it's raw metadata
|
|
120
|
+
const tagCsvLen = new DataView(rawMeta.buffer, rawMeta.byteOffset, 4).getUint32(0, true);
|
|
121
|
+
if (tagCsvLen < 4096 && tagCsvLen < rawMeta.length) {
|
|
122
|
+
tags = unwrapped.tags;
|
|
123
|
+
assetMeta = unwrapped.assetMetadata;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
assetMeta = rawMeta;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
assetMeta = rawMeta;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed = readStringIdAndDetails(entry.type, assetMeta);
|
|
134
|
+
stringId = parsed.stringId;
|
|
135
|
+
details = parsed.details;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Metadata format not recognized — show what we can
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
assets.push({ type: typeName, stringId, dataSize: entry.dataSize, tags, details });
|
|
142
|
+
}
|
|
143
|
+
// Sort by type then stringId
|
|
144
|
+
assets.sort((a, b) => a.type.localeCompare(b.type) || a.stringId.localeCompare(b.stringId));
|
|
145
|
+
// Print
|
|
146
|
+
const packName = path.basename(absPath);
|
|
147
|
+
const totalSize = buf.length;
|
|
148
|
+
console.log(`\nPack: ${packName} (${formatBytes(totalSize)}, v${version})`);
|
|
149
|
+
console.log(`Assets: ${assetCount}\n`);
|
|
150
|
+
// Calculate column widths
|
|
151
|
+
const typeWidth = Math.max(4, ...assets.map(a => a.type.length));
|
|
152
|
+
const idWidth = Math.max(8, ...assets.map(a => a.stringId.length));
|
|
153
|
+
for (const a of assets) {
|
|
154
|
+
const type = `[${a.type}]`.padEnd(typeWidth + 2);
|
|
155
|
+
const id = a.stringId.padEnd(idWidth);
|
|
156
|
+
const size = formatBytes(a.dataSize).padStart(10);
|
|
157
|
+
let line = ` ${type} ${id} ${size}`;
|
|
158
|
+
if (opts?.verbose && a.details)
|
|
159
|
+
line += ` ${a.details}`;
|
|
160
|
+
if (a.tags.length > 0 && a.tags[0] !== '')
|
|
161
|
+
line += ` [${a.tags.join(', ')}]`;
|
|
162
|
+
console.log(line);
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
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/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';
|
|
@@ -1473,10 +1569,12 @@ export async function startServer(opts = {}) {
|
|
|
1473
1569
|
}));
|
|
1474
1570
|
ws.on('close', () => wsClients.delete(ws));
|
|
1475
1571
|
});
|
|
1476
|
-
// Initialize worker pool
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1572
|
+
// Initialize worker pool (shared across servers in the same process)
|
|
1573
|
+
if (!workerPool) {
|
|
1574
|
+
workerPool = initWorkerPool(opts.wasmDir);
|
|
1575
|
+
}
|
|
1576
|
+
// Open project if specified (skip if already loaded — e.g. stowkit editor starts two servers)
|
|
1577
|
+
if (opts.projectDir && (!projectConfig || projectConfig.projectDir !== opts.projectDir)) {
|
|
1480
1578
|
await openProject(opts.projectDir);
|
|
1481
1579
|
queueProcessing();
|
|
1482
1580
|
}
|
package/package.json
CHANGED
package/skill.md
CHANGED
|
@@ -50,6 +50,7 @@ stowkit rename <path> <name> # Rename an asset file (preserves extension, upd
|
|
|
50
50
|
stowkit move <path> <folder> # Move an asset to a different folder (updates GLB child refs)
|
|
51
51
|
stowkit delete <path> # Delete an asset and its .stowmeta/.stowcache files
|
|
52
52
|
stowkit set-id <path> <id> # Change an asset's stringId
|
|
53
|
+
stowkit inspect <file.stow> # Show manifest of a built .stow pack (use -v for details)
|
|
53
54
|
stowkit packer [dir] # Open the packer GUI in browser
|
|
54
55
|
stowkit editor [dir] # Open the level editor in browser
|
|
55
56
|
stowkit serve [dir] # Start API server only (no GUI)
|