@series-inc/stowkit-cli 0.1.14 → 0.1.16
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/asset-commands.d.ts +4 -0
- package/dist/asset-commands.js +122 -0
- package/dist/cli.js +56 -0
- package/dist/create-material.d.ts +3 -0
- package/dist/create-material.js +47 -0
- package/dist/orchestrator.js +84 -122
- package/package.json +2 -2
- package/skill.md +43 -6
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function renameAsset(projectDir: string, assetPath: string, newName: string): Promise<void>;
|
|
2
|
+
export declare function moveAsset(projectDir: string, assetPath: string, targetFolder: string): Promise<void>;
|
|
3
|
+
export declare function deleteAsset(projectDir: string, assetPath: string): Promise<void>;
|
|
4
|
+
export declare function setStringId(projectDir: string, assetPath: string, newStringId: string): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { readProjectConfig, renameFile, deleteFile, readFile, } from './node-fs.js';
|
|
4
|
+
import { readStowmeta, writeStowmeta, } from './app/stowmeta-io.js';
|
|
5
|
+
// ─── Rename ──────────────────────────────────────────────────────────────────
|
|
6
|
+
export async function renameAsset(projectDir, assetPath, newName) {
|
|
7
|
+
const config = await readProjectConfig(projectDir);
|
|
8
|
+
// Build new ID: same folder, new filename (preserve original extension)
|
|
9
|
+
const folder = assetPath.includes('/') ? assetPath.slice(0, assetPath.lastIndexOf('/') + 1) : '';
|
|
10
|
+
const oldBase = assetPath.split('/').pop() ?? assetPath;
|
|
11
|
+
const extMatch = oldBase.match(/\.[^.]+$/);
|
|
12
|
+
const ext = extMatch ? extMatch[0] : '';
|
|
13
|
+
const fullNewName = ext && !newName.endsWith(ext) ? newName + ext : newName;
|
|
14
|
+
const newId = folder + fullNewName;
|
|
15
|
+
if (newId === assetPath) {
|
|
16
|
+
console.log('Nothing to rename — same name.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Verify source exists
|
|
20
|
+
const sourceData = await readFile(config.srcArtDir, assetPath);
|
|
21
|
+
if (!sourceData) {
|
|
22
|
+
console.error(`Asset not found: ${assetPath}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Rename source, stowmeta, stowcache
|
|
26
|
+
await renameFile(config.srcArtDir, assetPath, newId);
|
|
27
|
+
await renameFile(config.srcArtDir, `${assetPath}.stowmeta`, `${newId}.stowmeta`);
|
|
28
|
+
await renameFile(config.srcArtDir, `${assetPath}.stowcache`, `${newId}.stowcache`);
|
|
29
|
+
// For GLB containers, rename the .children cache directory
|
|
30
|
+
const meta = await readStowmeta(config.srcArtDir, newId);
|
|
31
|
+
if (meta && meta.type === 'glbContainer') {
|
|
32
|
+
await renameFile(config.srcArtDir, `${assetPath}.children`, `${newId}.children`);
|
|
33
|
+
}
|
|
34
|
+
console.log(`Renamed: ${assetPath} → ${newId}`);
|
|
35
|
+
}
|
|
36
|
+
// ─── Move ────────────────────────────────────────────────────────────────────
|
|
37
|
+
export async function moveAsset(projectDir, assetPath, targetFolder) {
|
|
38
|
+
const config = await readProjectConfig(projectDir);
|
|
39
|
+
const fileName = assetPath.split('/').pop() ?? assetPath;
|
|
40
|
+
const newId = targetFolder ? `${targetFolder}/${fileName}` : fileName;
|
|
41
|
+
if (newId === assetPath) {
|
|
42
|
+
console.log('Nothing to move — already in target folder.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Verify source exists
|
|
46
|
+
const sourceData = await readFile(config.srcArtDir, assetPath);
|
|
47
|
+
if (!sourceData) {
|
|
48
|
+
console.error(`Asset not found: ${assetPath}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// Move source, stowmeta, stowcache
|
|
52
|
+
await renameFile(config.srcArtDir, assetPath, newId);
|
|
53
|
+
await renameFile(config.srcArtDir, `${assetPath}.stowmeta`, `${newId}.stowmeta`);
|
|
54
|
+
await renameFile(config.srcArtDir, `${assetPath}.stowcache`, `${newId}.stowcache`);
|
|
55
|
+
// For GLB containers, move the .children cache directory and update child references
|
|
56
|
+
const meta = await readStowmeta(config.srcArtDir, newId);
|
|
57
|
+
if (meta && meta.type === 'glbContainer') {
|
|
58
|
+
await renameFile(config.srcArtDir, `${assetPath}.children`, `${newId}.children`);
|
|
59
|
+
// Update texture references in material configs
|
|
60
|
+
const glbMeta = meta;
|
|
61
|
+
const oldPrefix = assetPath + '/';
|
|
62
|
+
const newPrefix = newId + '/';
|
|
63
|
+
let updated = false;
|
|
64
|
+
for (const child of glbMeta.children ?? []) {
|
|
65
|
+
if (child.materialConfig) {
|
|
66
|
+
for (const prop of child.materialConfig.properties) {
|
|
67
|
+
if (prop.textureAsset?.startsWith(oldPrefix)) {
|
|
68
|
+
prop.textureAsset = newPrefix + prop.textureAsset.slice(oldPrefix.length);
|
|
69
|
+
updated = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (child.materialOverrides) {
|
|
74
|
+
for (const [k, v] of Object.entries(child.materialOverrides)) {
|
|
75
|
+
if (v?.startsWith(oldPrefix)) {
|
|
76
|
+
child.materialOverrides[k] = newPrefix + v.slice(oldPrefix.length);
|
|
77
|
+
updated = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (updated) {
|
|
83
|
+
await writeStowmeta(config.srcArtDir, newId, glbMeta);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log(`Moved: ${assetPath} → ${newId}`);
|
|
87
|
+
}
|
|
88
|
+
// ─── Delete ──────────────────────────────────────────────────────────────────
|
|
89
|
+
export async function deleteAsset(projectDir, assetPath) {
|
|
90
|
+
const config = await readProjectConfig(projectDir);
|
|
91
|
+
// Verify source exists
|
|
92
|
+
const sourceData = await readFile(config.srcArtDir, assetPath);
|
|
93
|
+
if (!sourceData) {
|
|
94
|
+
console.error(`Asset not found: ${assetPath}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
// For GLB containers, also delete the .children cache directory
|
|
98
|
+
const meta = await readStowmeta(config.srcArtDir, assetPath);
|
|
99
|
+
if (meta && meta.type === 'glbContainer') {
|
|
100
|
+
const childrenDir = path.join(config.srcArtDir, `${assetPath}.children`);
|
|
101
|
+
await fs.rm(childrenDir, { recursive: true, force: true }).catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
// Delete source, stowmeta, stowcache
|
|
104
|
+
await deleteFile(config.srcArtDir, assetPath);
|
|
105
|
+
await deleteFile(config.srcArtDir, `${assetPath}.stowmeta`);
|
|
106
|
+
await deleteFile(config.srcArtDir, `${assetPath}.stowcache`);
|
|
107
|
+
console.log(`Deleted: ${assetPath}`);
|
|
108
|
+
}
|
|
109
|
+
// ─── Set stringId ────────────────────────────────────────────────────────────
|
|
110
|
+
export async function setStringId(projectDir, assetPath, newStringId) {
|
|
111
|
+
const config = await readProjectConfig(projectDir);
|
|
112
|
+
const meta = await readStowmeta(config.srcArtDir, assetPath);
|
|
113
|
+
if (!meta) {
|
|
114
|
+
console.error(`No .stowmeta found for: ${assetPath}`);
|
|
115
|
+
console.error('Run `stowkit build` or `stowkit scan` first to generate it.');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const oldStringId = meta.stringId;
|
|
119
|
+
meta.stringId = newStringId;
|
|
120
|
+
await writeStowmeta(config.srcArtDir, assetPath, meta);
|
|
121
|
+
console.log(`Updated stringId: "${oldStringId}" → "${newStringId}" (${assetPath})`);
|
|
122
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import { fullBuild, scanProject, showStatus } from './orchestrator.js';
|
|
|
6
6
|
import { startServer } from './server.js';
|
|
7
7
|
import { initProject } from './init.js';
|
|
8
8
|
import { cleanupProject } from './cleanup.js';
|
|
9
|
+
import { createMaterial } from './create-material.js';
|
|
10
|
+
import { renameAsset, moveAsset, deleteAsset, setStringId } from './asset-commands.js';
|
|
9
11
|
const args = process.argv.slice(2);
|
|
10
12
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
function printUsage() {
|
|
@@ -17,6 +19,11 @@ Usage:
|
|
|
17
19
|
stowkit process [dir] Compress assets (respects cache)
|
|
18
20
|
stowkit status [dir] Show project summary, stale asset count
|
|
19
21
|
stowkit clean [dir] Delete orphaned .stowcache and .stowmeta files
|
|
22
|
+
stowkit create-material <path> Create a .stowmat material file
|
|
23
|
+
stowkit rename <path> <name> Rename an asset file
|
|
24
|
+
stowkit move <path> <folder> Move an asset to a different folder
|
|
25
|
+
stowkit delete <path> Delete an asset and its sidecar files
|
|
26
|
+
stowkit set-id <path> <id> Change an asset's stringId
|
|
20
27
|
stowkit packer [dir] Open the packer GUI
|
|
21
28
|
stowkit editor [dir] Open the level editor
|
|
22
29
|
stowkit serve [dir] Start API server only (no GUI)
|
|
@@ -25,6 +32,7 @@ Options:
|
|
|
25
32
|
--force Ignore cache and reprocess everything
|
|
26
33
|
--verbose Detailed output
|
|
27
34
|
--port Server port (default 3210)
|
|
35
|
+
--schema Material schema template: pbr (default), unlit, or custom name
|
|
28
36
|
--help Show this help message
|
|
29
37
|
`.trim());
|
|
30
38
|
}
|
|
@@ -94,6 +102,54 @@ async function main() {
|
|
|
94
102
|
case 'clean':
|
|
95
103
|
await cleanupProject(projectDir, { verbose });
|
|
96
104
|
break;
|
|
105
|
+
case 'create-material': {
|
|
106
|
+
// For create-material, the positional arg is the material path, not project dir
|
|
107
|
+
const matPath = args.find(a => !a.startsWith('-') && a !== command);
|
|
108
|
+
if (!matPath) {
|
|
109
|
+
console.error('Usage: stowkit create-material <path> [--schema pbr|unlit|<name>]');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const schemaIdx = args.indexOf('--schema');
|
|
113
|
+
const schema = schemaIdx >= 0 ? args[schemaIdx + 1] : undefined;
|
|
114
|
+
await createMaterial('.', matPath, { schema });
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'rename': {
|
|
118
|
+
const positional = args.filter(a => !a.startsWith('-') && a !== command);
|
|
119
|
+
if (positional.length < 2) {
|
|
120
|
+
console.error('Usage: stowkit rename <asset-path> <new-name>');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
await renameAsset('.', positional[0], positional[1]);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'move': {
|
|
127
|
+
const positional = args.filter(a => !a.startsWith('-') && a !== command);
|
|
128
|
+
if (positional.length < 2) {
|
|
129
|
+
console.error('Usage: stowkit move <asset-path> <target-folder>');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
await moveAsset('.', positional[0], positional[1]);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'delete': {
|
|
136
|
+
const assetPath = args.find(a => !a.startsWith('-') && a !== command);
|
|
137
|
+
if (!assetPath) {
|
|
138
|
+
console.error('Usage: stowkit delete <asset-path>');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
await deleteAsset('.', assetPath);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'set-id': {
|
|
145
|
+
const positional = args.filter(a => !a.startsWith('-') && a !== command);
|
|
146
|
+
if (positional.length < 2) {
|
|
147
|
+
console.error('Usage: stowkit set-id <asset-path> <new-string-id>');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
await setStringId('.', positional[0], positional[1]);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
97
153
|
case 'packer': {
|
|
98
154
|
const packerDir = resolveAppDir('@series-inc/stowkit-packer-gui', 'stowkit-packer-gui');
|
|
99
155
|
if (!packerDir) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { readProjectConfig } from './node-fs.js';
|
|
4
|
+
const TEMPLATES = {
|
|
5
|
+
pbr: [
|
|
6
|
+
{ fieldName: 'BaseColor', fieldType: 'texture', previewFlag: 'mainTex', value: [1, 1, 1, 1], textureAsset: null },
|
|
7
|
+
{ fieldName: 'Normal', fieldType: 'texture', previewFlag: 'none', value: [0, 0, 1, 0], textureAsset: null },
|
|
8
|
+
{ fieldName: 'Tint', fieldType: 'color', previewFlag: 'tint', value: [1, 1, 1, 1], textureAsset: null },
|
|
9
|
+
{ fieldName: 'AlphaTest', fieldType: 'float', previewFlag: 'alphaTest', value: [0.5, 0, 0, 0], textureAsset: null },
|
|
10
|
+
],
|
|
11
|
+
unlit: [
|
|
12
|
+
{ fieldName: 'BaseColor', fieldType: 'texture', previewFlag: 'mainTex', value: [1, 1, 1, 1], textureAsset: null },
|
|
13
|
+
{ fieldName: 'Tint', fieldType: 'color', previewFlag: 'tint', value: [1, 1, 1, 1], textureAsset: null },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
export async function createMaterial(projectDir, relativePath, opts) {
|
|
17
|
+
const config = await readProjectConfig(projectDir);
|
|
18
|
+
// Ensure .stowmat extension
|
|
19
|
+
if (!relativePath.endsWith('.stowmat')) {
|
|
20
|
+
relativePath += '.stowmat';
|
|
21
|
+
}
|
|
22
|
+
const fullPath = path.join(config.srcArtDir, relativePath);
|
|
23
|
+
// Don't overwrite existing
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(fullPath);
|
|
26
|
+
console.error(`Material already exists: ${relativePath}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Does not exist — good
|
|
31
|
+
}
|
|
32
|
+
const schemaName = opts?.schema ?? 'pbr';
|
|
33
|
+
const properties = TEMPLATES[schemaName] ?? [];
|
|
34
|
+
const mat = {
|
|
35
|
+
version: 1,
|
|
36
|
+
schemaName,
|
|
37
|
+
properties,
|
|
38
|
+
};
|
|
39
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
40
|
+
await fs.writeFile(fullPath, JSON.stringify(mat, null, 2) + '\n');
|
|
41
|
+
console.log(`Created material: ${relativePath}`);
|
|
42
|
+
console.log(` Schema: ${schemaName}`);
|
|
43
|
+
console.log(` Properties: ${properties.map(p => p.fieldName).join(', ') || '(none)'}`);
|
|
44
|
+
if (!TEMPLATES[schemaName]) {
|
|
45
|
+
console.log(` (Custom schema — no template properties. Edit the file to add properties.)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -98,58 +98,8 @@ export async function fullBuild(projectDir, opts) {
|
|
|
98
98
|
}
|
|
99
99
|
assets.push(asset);
|
|
100
100
|
assetsById.set(id, asset);
|
|
101
|
-
//
|
|
102
|
-
|
|
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
|
-
}
|
|
101
|
+
// GlbContainers are always extracted in section 2b (even if children exist in manifest)
|
|
102
|
+
// so that preserveHierarchy changes, re-exports, etc. are always reflected.
|
|
153
103
|
}
|
|
154
104
|
// Materials from .stowmat files
|
|
155
105
|
for (const matFile of scan.matFiles) {
|
|
@@ -178,10 +128,9 @@ export async function fullBuild(projectDir, opts) {
|
|
|
178
128
|
assets.push(asset);
|
|
179
129
|
assetsById.set(id, asset);
|
|
180
130
|
}
|
|
181
|
-
// 2b.
|
|
182
|
-
// Store extract results so mesh/animation children can be processed after encoder init
|
|
131
|
+
// 2b. Extract all GLB containers (always re-parse so preserveHierarchy etc. are reflected)
|
|
183
132
|
const glbExtracts = new Map();
|
|
184
|
-
const glbContainers = assets.filter(a => a.type === AssetType.GlbContainer
|
|
133
|
+
const glbContainers = assets.filter(a => a.type === AssetType.GlbContainer);
|
|
185
134
|
for (const container of glbContainers) {
|
|
186
135
|
try {
|
|
187
136
|
const sourceData = await readFile(config.srcArtDir, container.id);
|
|
@@ -237,8 +186,6 @@ export async function fullBuild(projectDir, opts) {
|
|
|
237
186
|
// Create child assets from inline entries
|
|
238
187
|
for (const child of childrenManifest) {
|
|
239
188
|
const childId = `${container.id}/${child.name}`;
|
|
240
|
-
if (assetsById.has(childId))
|
|
241
|
-
continue;
|
|
242
189
|
const baseName = child.name.replace(/\.[^.]+$/, '');
|
|
243
190
|
const { type: cType, settings: cSettings } = glbChildToAssetSettings(child);
|
|
244
191
|
const childAsset = {
|
|
@@ -274,6 +221,27 @@ export async function fullBuild(projectDir, opts) {
|
|
|
274
221
|
childAsset.processedSize = result.processedSize;
|
|
275
222
|
}
|
|
276
223
|
}
|
|
224
|
+
// Check cache for texture/mesh children (skip if --force)
|
|
225
|
+
if (childAsset.status === 'pending' && !force && child.cache) {
|
|
226
|
+
const cached = await readCacheBlobs(config.srcArtDir, childId);
|
|
227
|
+
if (cached) {
|
|
228
|
+
for (const [key, data] of cached) {
|
|
229
|
+
if (key === `${childId}:__metadata__`) {
|
|
230
|
+
try {
|
|
231
|
+
childAsset.metadata = JSON.parse(new TextDecoder().decode(data));
|
|
232
|
+
}
|
|
233
|
+
catch { /* skip */ }
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
BlobStore.setProcessed(key, data);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
childAsset.status = 'ready';
|
|
240
|
+
childAsset.processedSize = BlobStore.getProcessed(childId)?.length ?? 0;
|
|
241
|
+
if (verbose)
|
|
242
|
+
console.log(` [cached] ${childId}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
277
245
|
assets.push(childAsset);
|
|
278
246
|
assetsById.set(childId, childAsset);
|
|
279
247
|
}
|
|
@@ -318,10 +286,9 @@ export async function fullBuild(projectDir, opts) {
|
|
|
318
286
|
console.error(` [glb] ${container.id} FAILED: ${container.error}`);
|
|
319
287
|
}
|
|
320
288
|
}
|
|
321
|
-
// 2c. Process
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const totalWork = glbMeshChildren.length + pending.length;
|
|
289
|
+
// 2c. Process all pending assets via worker pool (one queue, no split)
|
|
290
|
+
const pending = assets.filter(a => a.status === 'pending');
|
|
291
|
+
const totalWork = pending.length;
|
|
325
292
|
if (totalWork === 0) {
|
|
326
293
|
if (verbose)
|
|
327
294
|
console.log('All assets cached, nothing to process.');
|
|
@@ -330,39 +297,34 @@ export async function fullBuild(projectDir, opts) {
|
|
|
330
297
|
console.log(`Processing ${totalWork} asset(s)...`);
|
|
331
298
|
const pool = new WorkerPool({ wasmDir: opts?.wasmDir });
|
|
332
299
|
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
|
|
359
300
|
const queue = [...pending];
|
|
360
301
|
async function processNext() {
|
|
361
302
|
while (queue.length > 0) {
|
|
362
303
|
const asset = queue.shift();
|
|
363
304
|
const id = asset.id;
|
|
364
305
|
try {
|
|
365
|
-
//
|
|
306
|
+
// GLB mesh children use processExtractedMesh (needs parsed mesh data)
|
|
307
|
+
const isMeshChild = asset.parentId &&
|
|
308
|
+
(asset.type === AssetType.StaticMesh || asset.type === AssetType.SkinnedMesh);
|
|
309
|
+
if (isMeshChild) {
|
|
310
|
+
const extract = glbExtracts.get(asset.parentId);
|
|
311
|
+
if (!extract)
|
|
312
|
+
throw new Error(`No extract for parent ${asset.parentId}`);
|
|
313
|
+
const mesh = extract.meshes.find(m => `${asset.parentId}/${m.name}` === id);
|
|
314
|
+
if (!mesh)
|
|
315
|
+
throw new Error(`Mesh not found in extract: ${id}`);
|
|
316
|
+
const { result, blobs } = await pool.processExtractedMesh({ childId: id, imported: mesh.imported, hasSkeleton: mesh.hasSkeleton, stringId: asset.stringId, settings: asset.settings });
|
|
317
|
+
for (const [key, data] of blobs)
|
|
318
|
+
BlobStore.setProcessed(key, data);
|
|
319
|
+
asset.status = 'ready';
|
|
320
|
+
asset.metadata = result.metadata;
|
|
321
|
+
asset.processedSize = result.processedSize;
|
|
322
|
+
processed++;
|
|
323
|
+
if (verbose)
|
|
324
|
+
console.log(` [${processed}/${totalWork}] ${id} (glb-mesh)`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Everything else uses processAsset (source bytes)
|
|
366
328
|
let sourceData = BlobStore.getSource(id);
|
|
367
329
|
if (!sourceData) {
|
|
368
330
|
const data = await readFile(config.srcArtDir, id);
|
|
@@ -381,39 +343,39 @@ export async function fullBuild(projectDir, opts) {
|
|
|
381
343
|
asset.processedSize = result.processedSize;
|
|
382
344
|
processed++;
|
|
383
345
|
console.log(` [${processed}/${totalWork}] ${id} (${elapsed}ms)`);
|
|
384
|
-
// Write cache
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
346
|
+
// Write cache (only for top-level assets that have their own file on disk)
|
|
347
|
+
if (!asset.parentId) {
|
|
348
|
+
const cacheEntries = new Map();
|
|
349
|
+
const processedBlob = BlobStore.getProcessed(id);
|
|
350
|
+
if (processedBlob)
|
|
351
|
+
cacheEntries.set(id, processedBlob);
|
|
352
|
+
if (result.metadata) {
|
|
353
|
+
cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(result.metadata)));
|
|
354
|
+
}
|
|
355
|
+
for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
|
|
356
|
+
const blob = BlobStore.getProcessed(`${id}${suffix}`);
|
|
357
|
+
if (blob)
|
|
358
|
+
cacheEntries.set(`${id}${suffix}`, blob);
|
|
359
|
+
}
|
|
360
|
+
const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
|
|
361
|
+
const clipCount = animCountBlob ? animCountBlob[0] : 0;
|
|
362
|
+
for (let ci = 0; ci < clipCount; ci++) {
|
|
363
|
+
const animData = BlobStore.getProcessed(`${id}:anim:${ci}`);
|
|
364
|
+
if (animData)
|
|
365
|
+
cacheEntries.set(`${id}:anim:${ci}`, animData);
|
|
366
|
+
const animMeta = BlobStore.getProcessed(`${id}:animMeta:${ci}`);
|
|
367
|
+
if (animMeta)
|
|
368
|
+
cacheEntries.set(`${id}:animMeta:${ci}`, animMeta);
|
|
369
|
+
}
|
|
370
|
+
if (cacheEntries.size > 0) {
|
|
371
|
+
await writeCacheBlobs(config.srcArtDir, id, cacheEntries);
|
|
372
|
+
const snapshot = await getFileSnapshot(config.srcArtDir, id);
|
|
373
|
+
if (snapshot) {
|
|
374
|
+
const meta = await readStowmeta(config.srcArtDir, id);
|
|
375
|
+
if (meta) {
|
|
376
|
+
meta.cache = buildCacheStamp(snapshot, asset.type, asset.settings);
|
|
377
|
+
await writeStowmeta(config.srcArtDir, id, meta);
|
|
378
|
+
}
|
|
417
379
|
}
|
|
418
380
|
}
|
|
419
381
|
}
|
|
@@ -427,7 +389,7 @@ export async function fullBuild(projectDir, opts) {
|
|
|
427
389
|
}
|
|
428
390
|
}
|
|
429
391
|
const workers = [];
|
|
430
|
-
for (let i = 0; i < Math.min(maxConcurrent,
|
|
392
|
+
for (let i = 0; i < Math.min(maxConcurrent, queue.length); i++) {
|
|
431
393
|
workers.push(processNext());
|
|
432
394
|
}
|
|
433
395
|
await Promise.all(workers);
|
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.16",
|
|
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.9",
|
|
21
21
|
"@series-inc/stowkit-editor": "^0.1.2",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|
package/skill.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
StowKit is a game asset pipeline that compresses and packs assets into `.stow` binary files for runtime loading.
|
|
4
4
|
|
|
5
|
+
## CRITICAL: Never manually create .stowmeta files
|
|
6
|
+
|
|
7
|
+
`.stowmeta` files are **auto-generated by the CLI**. To add any asset to the project:
|
|
8
|
+
|
|
9
|
+
1. Place the source file (GLB, PNG, FBX, WAV, etc.) into the `srcArtDir`
|
|
10
|
+
2. Run `npx stowkit build` (or `npx stowkit scan`)
|
|
11
|
+
3. The CLI detects the new file, generates the correct `.stowmeta` with proper defaults, and processes it
|
|
12
|
+
|
|
13
|
+
**Do not write `.stowmeta` files by hand.** Only edit an existing `.stowmeta` after it has been generated by the CLI (e.g. to change quality settings, pack assignment, or stringId). The same applies to GLB children — the `children` array is populated automatically on the first build.
|
|
14
|
+
|
|
15
|
+
To create materials, use `npx stowkit create-material <path>` — this scaffolds a `.stowmat` file with the right structure. You can also create `.stowmat` files manually if needed.
|
|
16
|
+
|
|
5
17
|
## Project Structure
|
|
6
18
|
|
|
7
19
|
A StowKit project has a `.felicityproject` JSON file at its root:
|
|
@@ -30,6 +42,11 @@ npx stowkit scan [dir] # Detect new assets and generate .stowmeta d
|
|
|
30
42
|
npx stowkit process [dir] # Compress assets (respects cache)
|
|
31
43
|
npx stowkit status [dir] # Show project summary, stale asset count
|
|
32
44
|
npx stowkit clean [dir] # Delete orphaned .stowcache and .stowmeta files
|
|
45
|
+
npx stowkit create-material <path> # Create a .stowmat material file (--schema pbr|unlit|<name>)
|
|
46
|
+
npx stowkit rename <path> <name> # Rename an asset file (preserves extension, updates sidecars)
|
|
47
|
+
npx stowkit move <path> <folder> # Move an asset to a different folder (updates GLB child refs)
|
|
48
|
+
npx stowkit delete <path> # Delete an asset and its .stowmeta/.stowcache files
|
|
49
|
+
npx stowkit set-id <path> <id> # Change an asset's stringId
|
|
33
50
|
npx stowkit packer [dir] # Open the packer GUI in browser
|
|
34
51
|
npx stowkit editor [dir] # Open the level editor in browser
|
|
35
52
|
npx stowkit serve [dir] # Start API server only (no GUI)
|
|
@@ -41,6 +58,7 @@ All commands default to the current directory.
|
|
|
41
58
|
- `--force` — Ignore cache and reprocess everything
|
|
42
59
|
- `--verbose` / `-v` — Detailed output
|
|
43
60
|
- `--port <number>` — Server port (default 3210)
|
|
61
|
+
- `--schema <name>` — Material schema template for `create-material` (default: `pbr`)
|
|
44
62
|
|
|
45
63
|
## Supported Asset Types
|
|
46
64
|
|
|
@@ -56,9 +74,9 @@ All commands default to the current directory.
|
|
|
56
74
|
|
|
57
75
|
**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
76
|
|
|
59
|
-
## .stowmeta Files
|
|
77
|
+
## .stowmeta Files (auto-generated — do not create manually)
|
|
60
78
|
|
|
61
|
-
Every source asset gets a `.stowmeta` sidecar file
|
|
79
|
+
Every source asset gets a `.stowmeta` sidecar file generated by `npx stowkit build` or `npx stowkit scan`. These files control processing settings and should only be **edited** (never created) by hand. The examples below are reference for understanding and editing existing files.
|
|
62
80
|
|
|
63
81
|
**Texture example:**
|
|
64
82
|
```json
|
|
@@ -347,6 +365,18 @@ Add `*.stowcache` to `.gitignore`.
|
|
|
347
365
|
5. To exclude a child from packing, set `"excluded": true` on that child entry
|
|
348
366
|
6. To preserve the scene graph hierarchy in static meshes, set `"preserveHierarchy": true` on the container
|
|
349
367
|
|
|
368
|
+
### Enabling preserveHierarchy from CLI
|
|
369
|
+
|
|
370
|
+
To enable hierarchy preservation on a GLB, edit its `.stowmeta` and set `"preserveHierarchy": true`, then rebuild:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# After the first build has generated the .stowmeta, edit it:
|
|
374
|
+
# Set "preserveHierarchy": true in assets/models/hero.glb.stowmeta
|
|
375
|
+
npx stowkit build
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The CLI always re-extracts GLB containers on every build, so changes to `preserveHierarchy` take effect immediately — no `--force` needed.
|
|
379
|
+
|
|
350
380
|
### When to use preserveHierarchy
|
|
351
381
|
|
|
352
382
|
- **Default (false):** All mesh geometry is baked to world space and flattened into a single mesh. Use this for simple props and environment pieces.
|
|
@@ -355,10 +385,17 @@ Add `*.stowcache` to `.gitignore`.
|
|
|
355
385
|
|
|
356
386
|
### Other common tasks
|
|
357
387
|
|
|
358
|
-
- **Add a texture:**
|
|
359
|
-
- **
|
|
360
|
-
- **
|
|
361
|
-
- **
|
|
388
|
+
- **Add a texture:** Place PNG/JPG into `assets/`, run `npx stowkit build`. The CLI auto-generates the `.stowmeta`. Do NOT create it yourself.
|
|
389
|
+
- **Add audio:** Place WAV/MP3/OGG into `assets/`, run `npx stowkit build`. Same rule — never manually create `.stowmeta`.
|
|
390
|
+
- **Add an FBX mesh:** Place FBX into `assets/`, run `npx stowkit build`.
|
|
391
|
+
- **Enable preserve hierarchy on a GLB:** Edit the `.stowmeta` for the GLB container, set `"preserveHierarchy": true`, then `npx stowkit build`
|
|
392
|
+
- **Change compression quality:** Edit the **existing** `.stowmeta` file's quality/resize fields (after it was generated by a build/scan), then `npx stowkit build`
|
|
393
|
+
- **Create a material:** Run `npx stowkit create-material materials/MyMat` (creates `assets/materials/MyMat.stowmat` with PBR template). Use `--schema unlit` for unlit materials, or `--schema <name>` for a custom schema. Then run `npx stowkit build`.
|
|
394
|
+
- **Assign material to mesh:** Edit the mesh's **existing** `.stowmeta` to add `materialOverrides`
|
|
395
|
+
- **Rename an asset:** `npx stowkit rename textures/old_name.png new_name` (extension preserved automatically, sidecars renamed too)
|
|
396
|
+
- **Move an asset:** `npx stowkit move textures/hero.png characters` (moves to `characters/hero.png`, updates GLB child refs if container)
|
|
397
|
+
- **Delete an asset:** `npx stowkit delete textures/unused.png` (removes source + .stowmeta + .stowcache, cascades for GLB containers)
|
|
398
|
+
- **Change an asset's stringId:** `npx stowkit set-id textures/hero.png hero_diffuse`
|
|
362
399
|
- **Check project health:** Run `npx stowkit status`
|
|
363
400
|
- **Full rebuild:** `npx stowkit build --force`
|
|
364
401
|
- **Clean orphaned files:** `npx stowkit clean`
|