@series-inc/stowkit-cli 0.1.0
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 +9 -0
- package/dist/app/blob-store.js +42 -0
- package/dist/app/disk-project.d.ts +84 -0
- package/dist/app/disk-project.js +70 -0
- package/dist/app/process-cache.d.ts +10 -0
- package/dist/app/process-cache.js +126 -0
- package/dist/app/state.d.ts +38 -0
- package/dist/app/state.js +16 -0
- package/dist/app/stowmat-io.d.ts +6 -0
- package/dist/app/stowmat-io.js +48 -0
- package/dist/app/stowmeta-io.d.ts +14 -0
- package/dist/app/stowmeta-io.js +207 -0
- package/dist/cleanup.d.ts +3 -0
- package/dist/cleanup.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +148 -0
- package/dist/core/binary.d.ts +41 -0
- package/dist/core/binary.js +118 -0
- package/dist/core/constants.d.ts +64 -0
- package/dist/core/constants.js +65 -0
- package/dist/core/path.d.ts +3 -0
- package/dist/core/path.js +27 -0
- package/dist/core/types.d.ts +204 -0
- package/dist/core/types.js +76 -0
- package/dist/encoders/aac-encoder.d.ts +12 -0
- package/dist/encoders/aac-encoder.js +179 -0
- package/dist/encoders/basis-encoder.d.ts +15 -0
- package/dist/encoders/basis-encoder.js +116 -0
- package/dist/encoders/draco-encoder.d.ts +11 -0
- package/dist/encoders/draco-encoder.js +155 -0
- package/dist/encoders/fbx-loader.d.ts +4 -0
- package/dist/encoders/fbx-loader.js +540 -0
- package/dist/encoders/image-decoder.d.ts +13 -0
- package/dist/encoders/image-decoder.js +33 -0
- package/dist/encoders/interfaces.d.ts +105 -0
- package/dist/encoders/interfaces.js +1 -0
- package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
- package/dist/encoders/skinned-mesh-builder.js +135 -0
- package/dist/format/metadata.d.ts +18 -0
- package/dist/format/metadata.js +381 -0
- package/dist/format/packer.d.ts +8 -0
- package/dist/format/packer.js +87 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +35 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +73 -0
- package/dist/node-fs.d.ts +22 -0
- package/dist/node-fs.js +148 -0
- package/dist/orchestrator.d.ts +20 -0
- package/dist/orchestrator.js +301 -0
- package/dist/pipeline.d.ts +23 -0
- package/dist/pipeline.js +354 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +859 -0
- package/package.json +35 -0
- package/skill.md +211 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Core types and constants
|
|
2
|
+
export * from './core/types.js';
|
|
3
|
+
export * from './core/constants.js';
|
|
4
|
+
export * from './core/binary.js';
|
|
5
|
+
export * from './core/path.js';
|
|
6
|
+
// Format
|
|
7
|
+
export { StowPacker } from './format/packer.js';
|
|
8
|
+
export * from './format/metadata.js';
|
|
9
|
+
// App layer
|
|
10
|
+
export * from './app/disk-project.js';
|
|
11
|
+
export { BlobStore } from './app/blob-store.js';
|
|
12
|
+
export * from './app/state.js';
|
|
13
|
+
export * from './app/stowmeta-io.js';
|
|
14
|
+
export * from './app/stowmat-io.js';
|
|
15
|
+
export * from './app/process-cache.js';
|
|
16
|
+
// Node file I/O
|
|
17
|
+
export * from './node-fs.js';
|
|
18
|
+
// Encoders
|
|
19
|
+
export * from './encoders/interfaces.js';
|
|
20
|
+
export { NodeBasisEncoder } from './encoders/basis-encoder.js';
|
|
21
|
+
export { NodeDracoEncoder, dracoPresetToSettings } from './encoders/draco-encoder.js';
|
|
22
|
+
export { NodeAacEncoder, NodeAudioDecoder } from './encoders/aac-encoder.js';
|
|
23
|
+
export { NodeFbxImporter } from './encoders/fbx-loader.js';
|
|
24
|
+
export { SharpImageDecoder } from './encoders/image-decoder.js';
|
|
25
|
+
export { buildSkinnedMeshData } from './encoders/skinned-mesh-builder.js';
|
|
26
|
+
// Pipeline
|
|
27
|
+
export { processAsset, buildPack, buildAnimationDataBlobsV2 } from './pipeline.js';
|
|
28
|
+
// Orchestrator
|
|
29
|
+
export { fullBuild, scanProject, showStatus } from './orchestrator.js';
|
|
30
|
+
// Server
|
|
31
|
+
export { startServer } from './server.js';
|
|
32
|
+
// Init
|
|
33
|
+
export { initProject } from './init.js';
|
|
34
|
+
// Cleanup
|
|
35
|
+
export { cleanupProject } from './cleanup.js';
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initProject(projectDir: string): Promise<void>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export async function initProject(projectDir) {
|
|
5
|
+
const absDir = path.resolve(projectDir);
|
|
6
|
+
// Check if already initialized
|
|
7
|
+
const configPath = path.join(absDir, '.felicityproject');
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(configPath);
|
|
10
|
+
console.log(`Project already initialized at ${absDir}`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Does not exist — create it
|
|
15
|
+
}
|
|
16
|
+
// Create srcArtDir with .gitignore for cache files
|
|
17
|
+
const srcArtDir = 'assets';
|
|
18
|
+
await fs.mkdir(path.join(absDir, srcArtDir), { recursive: true });
|
|
19
|
+
const assetsGitignore = path.join(absDir, srcArtDir, '.gitignore');
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(assetsGitignore);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
await fs.writeFile(assetsGitignore, '*.stowcache\n');
|
|
25
|
+
}
|
|
26
|
+
// Create public/cdn-assets output dir
|
|
27
|
+
await fs.mkdir(path.join(absDir, 'public/cdn-assets'), { recursive: true });
|
|
28
|
+
// Write .felicityproject
|
|
29
|
+
const config = {
|
|
30
|
+
srcArtDir,
|
|
31
|
+
name: path.basename(absDir),
|
|
32
|
+
cdnAssetsPath: 'public/cdn-assets',
|
|
33
|
+
packs: [{ name: 'default' }],
|
|
34
|
+
};
|
|
35
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
36
|
+
// Add stowkit artifacts to .gitignore
|
|
37
|
+
const gitignorePath = path.join(absDir, '.gitignore');
|
|
38
|
+
const stowkitIgnores = [
|
|
39
|
+
'# StowKit',
|
|
40
|
+
'*.stowcache',
|
|
41
|
+
'public/cdn-assets/',
|
|
42
|
+
].join('\n');
|
|
43
|
+
try {
|
|
44
|
+
const existing = await fs.readFile(gitignorePath, 'utf-8');
|
|
45
|
+
if (!existing.includes('*.stowcache')) {
|
|
46
|
+
await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + stowkitIgnores + '\n');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
await fs.writeFile(gitignorePath, stowkitIgnores + '\n');
|
|
51
|
+
}
|
|
52
|
+
// Copy Claude skill file
|
|
53
|
+
const skillDir = path.join(absDir, '.claude', 'skills');
|
|
54
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
55
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const skillSrc = path.resolve(thisDir, '../skill.md');
|
|
57
|
+
const skillDst = path.join(skillDir, 'stowkit.md');
|
|
58
|
+
try {
|
|
59
|
+
const skillContent = await fs.readFile(skillSrc, 'utf-8');
|
|
60
|
+
await fs.writeFile(skillDst, skillContent);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Skill file not found in package — skip silently
|
|
64
|
+
}
|
|
65
|
+
console.log(`Initialized StowKit project at ${absDir}`);
|
|
66
|
+
console.log(` Source art dir: ${srcArtDir}/`);
|
|
67
|
+
console.log(` Output dir: public/cdn-assets/`);
|
|
68
|
+
console.log(` Config: .felicityproject`);
|
|
69
|
+
console.log(` Claude skill: .claude/skills/stowkit.md`);
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
|
|
72
|
+
console.log('Then run: npx stowkit build');
|
|
73
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FelicityProject, FileSnapshot } from './app/disk-project.js';
|
|
2
|
+
export declare function readFile(basePath: string, relativePath: string): Promise<Uint8Array | null>;
|
|
3
|
+
export declare function readTextFile(basePath: string, relativePath: string): Promise<string | null>;
|
|
4
|
+
export declare function writeFile(basePath: string, relativePath: string, data: string | Uint8Array): Promise<void>;
|
|
5
|
+
export declare function deleteFile(basePath: string, relativePath: string): Promise<void>;
|
|
6
|
+
export declare function fileExists(basePath: string, relativePath: string): Promise<boolean>;
|
|
7
|
+
export declare function getFileSnapshot(basePath: string, relativePath: string): Promise<FileSnapshot | null>;
|
|
8
|
+
export interface ScanResult {
|
|
9
|
+
sourceFiles: FileSnapshot[];
|
|
10
|
+
metaFiles: FileSnapshot[];
|
|
11
|
+
matFiles: FileSnapshot[];
|
|
12
|
+
folders: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function scanDirectory(basePath: string): Promise<ScanResult>;
|
|
15
|
+
export interface ProjectConfig {
|
|
16
|
+
projectDir: string;
|
|
17
|
+
srcArtDir: string;
|
|
18
|
+
config: FelicityProject;
|
|
19
|
+
projectName: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function readProjectConfig(projectDir: string): Promise<ProjectConfig>;
|
|
22
|
+
export declare function writeFelicityProject(projectDir: string, config: FelicityProject): Promise<void>;
|
package/dist/node-fs.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
// ─── File I/O ────────────────────────────────────────────────────────────────
|
|
4
|
+
export async function readFile(basePath, relativePath) {
|
|
5
|
+
try {
|
|
6
|
+
const fullPath = path.join(basePath, relativePath);
|
|
7
|
+
const buf = await fs.readFile(fullPath);
|
|
8
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function readTextFile(basePath, relativePath) {
|
|
15
|
+
try {
|
|
16
|
+
const fullPath = path.join(basePath, relativePath);
|
|
17
|
+
return await fs.readFile(fullPath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeFile(basePath, relativePath, data) {
|
|
24
|
+
const fullPath = path.join(basePath, relativePath);
|
|
25
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
26
|
+
await fs.writeFile(fullPath, data);
|
|
27
|
+
}
|
|
28
|
+
export async function deleteFile(basePath, relativePath) {
|
|
29
|
+
try {
|
|
30
|
+
const fullPath = path.join(basePath, relativePath);
|
|
31
|
+
await fs.unlink(fullPath);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Ignore — file may not exist
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function fileExists(basePath, relativePath) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(path.join(basePath, relativePath));
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function getFileSnapshot(basePath, relativePath) {
|
|
47
|
+
try {
|
|
48
|
+
const fullPath = path.join(basePath, relativePath);
|
|
49
|
+
const stat = await fs.stat(fullPath);
|
|
50
|
+
return {
|
|
51
|
+
relativePath,
|
|
52
|
+
size: stat.size,
|
|
53
|
+
lastModified: stat.mtimeMs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ─── Directory Scanning ──────────────────────────────────────────────────────
|
|
61
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
62
|
+
'png', 'jpg', 'jpeg', 'bmp', 'tga', 'webp', 'gif',
|
|
63
|
+
'wav', 'mp3', 'ogg', 'flac', 'aac', 'm4a',
|
|
64
|
+
'fbx', 'obj', 'gltf', 'glb',
|
|
65
|
+
]);
|
|
66
|
+
function isSourceFile(name) {
|
|
67
|
+
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
|
68
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
69
|
+
}
|
|
70
|
+
function isMetaFile(name) {
|
|
71
|
+
return name.endsWith('.stowmeta');
|
|
72
|
+
}
|
|
73
|
+
function isMatFile(name) {
|
|
74
|
+
return name.endsWith('.stowmat');
|
|
75
|
+
}
|
|
76
|
+
export async function scanDirectory(basePath) {
|
|
77
|
+
const sourceFiles = [];
|
|
78
|
+
const metaFiles = [];
|
|
79
|
+
const matFiles = [];
|
|
80
|
+
const folders = [];
|
|
81
|
+
await walkDirectory(basePath, '', sourceFiles, metaFiles, matFiles, folders);
|
|
82
|
+
return { sourceFiles, metaFiles, matFiles, folders };
|
|
83
|
+
}
|
|
84
|
+
async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles, folders) {
|
|
85
|
+
const dirPath = prefix ? path.join(basePath, prefix) : basePath;
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
if (entry.name.startsWith('.'))
|
|
97
|
+
continue;
|
|
98
|
+
folders.push(relativePath);
|
|
99
|
+
await walkDirectory(basePath, relativePath, sourceFiles, metaFiles, matFiles, folders);
|
|
100
|
+
}
|
|
101
|
+
else if (entry.isFile()) {
|
|
102
|
+
const fullPath = path.join(basePath, relativePath);
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = await fs.stat(fullPath);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const snapshot = {
|
|
111
|
+
relativePath,
|
|
112
|
+
size: stat.size,
|
|
113
|
+
lastModified: stat.mtimeMs,
|
|
114
|
+
};
|
|
115
|
+
if (isMetaFile(entry.name)) {
|
|
116
|
+
metaFiles.push(snapshot);
|
|
117
|
+
}
|
|
118
|
+
else if (isMatFile(entry.name)) {
|
|
119
|
+
matFiles.push(snapshot);
|
|
120
|
+
}
|
|
121
|
+
else if (isSourceFile(entry.name)) {
|
|
122
|
+
sourceFiles.push(snapshot);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export async function readProjectConfig(projectDir) {
|
|
128
|
+
const configPath = path.join(projectDir, '.felicityproject');
|
|
129
|
+
let configText;
|
|
130
|
+
try {
|
|
131
|
+
configText = await fs.readFile(configPath, 'utf-8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error(`No .felicityproject found in ${projectDir}. ` +
|
|
135
|
+
`Create one with {"srcArtDir": "."} to use the directory as a project.`);
|
|
136
|
+
}
|
|
137
|
+
const config = JSON.parse(configText);
|
|
138
|
+
if (!config.srcArtDir) {
|
|
139
|
+
throw new Error('.felicityproject is missing "srcArtDir" field.');
|
|
140
|
+
}
|
|
141
|
+
const srcArtDir = path.resolve(projectDir, config.srcArtDir);
|
|
142
|
+
const projectName = config.name || path.basename(projectDir);
|
|
143
|
+
return { projectDir, srcArtDir, config, projectName };
|
|
144
|
+
}
|
|
145
|
+
export async function writeFelicityProject(projectDir, config) {
|
|
146
|
+
const configPath = path.join(projectDir, '.felicityproject');
|
|
147
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
148
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface BuildOptions {
|
|
2
|
+
force?: boolean;
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
maxConcurrent?: number;
|
|
5
|
+
wasmDir?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ScanReport {
|
|
8
|
+
projectName: string;
|
|
9
|
+
srcArtDir: string;
|
|
10
|
+
sourceFiles: number;
|
|
11
|
+
metaFiles: number;
|
|
12
|
+
matFiles: number;
|
|
13
|
+
newFiles: string[];
|
|
14
|
+
totalAssets: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function scanProject(projectDir: string, opts?: {
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
}): Promise<ScanReport>;
|
|
19
|
+
export declare function fullBuild(projectDir: string, opts?: BuildOptions): Promise<void>;
|
|
20
|
+
export declare function showStatus(projectDir: string): Promise<void>;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { AssetType } from './core/types.js';
|
|
4
|
+
import { cleanupProject } from './cleanup.js';
|
|
5
|
+
import { defaultAssetSettings } from './app/state.js';
|
|
6
|
+
import { BlobStore } from './app/blob-store.js';
|
|
7
|
+
import { readProjectConfig, scanDirectory, readFile, getFileSnapshot, } from './node-fs.js';
|
|
8
|
+
import { readStowmeta, writeStowmeta, stowmetaToAssetSettings, generateDefaultStowmeta, } from './app/stowmeta-io.js';
|
|
9
|
+
import { readStowmat, stowmatToMaterialConfig } from './app/stowmat-io.js';
|
|
10
|
+
import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, } from './app/process-cache.js';
|
|
11
|
+
import { processAsset, buildPack } from './pipeline.js';
|
|
12
|
+
// ─── Encoder initialization ────────────────────────────────────────────────
|
|
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';
|
|
18
|
+
export async function scanProject(projectDir, opts) {
|
|
19
|
+
const config = await readProjectConfig(projectDir);
|
|
20
|
+
const scan = await scanDirectory(config.srcArtDir);
|
|
21
|
+
const verbose = opts?.verbose ?? false;
|
|
22
|
+
const existingMeta = new Set(scan.metaFiles.map(f => f.relativePath.replace(/\.stowmeta$/, '')));
|
|
23
|
+
const newFiles = [];
|
|
24
|
+
for (const file of scan.sourceFiles) {
|
|
25
|
+
if (!existingMeta.has(file.relativePath)) {
|
|
26
|
+
newFiles.push(file.relativePath);
|
|
27
|
+
const meta = generateDefaultStowmeta(file.relativePath);
|
|
28
|
+
await writeStowmeta(config.srcArtDir, file.relativePath, meta);
|
|
29
|
+
if (verbose)
|
|
30
|
+
console.log(` [new] ${file.relativePath} → .stowmeta (${meta.type})`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
projectName: config.projectName,
|
|
35
|
+
srcArtDir: config.srcArtDir,
|
|
36
|
+
sourceFiles: scan.sourceFiles.length,
|
|
37
|
+
metaFiles: scan.metaFiles.length,
|
|
38
|
+
matFiles: scan.matFiles.length,
|
|
39
|
+
newFiles,
|
|
40
|
+
totalAssets: scan.sourceFiles.length + scan.matFiles.length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ─── Full Build ──────────────────────────────────────────────────────────────
|
|
44
|
+
export async function fullBuild(projectDir, opts) {
|
|
45
|
+
const verbose = opts?.verbose ?? false;
|
|
46
|
+
const force = opts?.force ?? false;
|
|
47
|
+
const maxConcurrent = opts?.maxConcurrent ?? 8;
|
|
48
|
+
const config = await readProjectConfig(projectDir);
|
|
49
|
+
if (verbose)
|
|
50
|
+
console.log(`Project: ${config.projectName}`);
|
|
51
|
+
if (verbose)
|
|
52
|
+
console.log(`Source dir: ${config.srcArtDir}`);
|
|
53
|
+
// 1. Scan
|
|
54
|
+
const scan = await scanDirectory(config.srcArtDir);
|
|
55
|
+
if (verbose)
|
|
56
|
+
console.log(`Found ${scan.sourceFiles.length} source files, ${scan.matFiles.length} materials`);
|
|
57
|
+
// 2. Build asset list
|
|
58
|
+
const assets = [];
|
|
59
|
+
const assetsById = new Map();
|
|
60
|
+
for (const file of scan.sourceFiles) {
|
|
61
|
+
const id = file.relativePath;
|
|
62
|
+
let meta = await readStowmeta(config.srcArtDir, id);
|
|
63
|
+
if (!meta) {
|
|
64
|
+
meta = generateDefaultStowmeta(id);
|
|
65
|
+
await writeStowmeta(config.srcArtDir, id, meta);
|
|
66
|
+
}
|
|
67
|
+
const { type, settings } = stowmetaToAssetSettings(meta);
|
|
68
|
+
const fileName = id.split('/').pop() ?? id;
|
|
69
|
+
const asset = {
|
|
70
|
+
id,
|
|
71
|
+
fileName,
|
|
72
|
+
stringId: meta.stringId,
|
|
73
|
+
type,
|
|
74
|
+
status: 'pending',
|
|
75
|
+
settings,
|
|
76
|
+
sourceSize: file.size,
|
|
77
|
+
processedSize: 0,
|
|
78
|
+
};
|
|
79
|
+
// Check cache
|
|
80
|
+
if (!force && meta.cache) {
|
|
81
|
+
const snapshot = await getFileSnapshot(config.srcArtDir, id);
|
|
82
|
+
if (snapshot && isCacheValid(meta, snapshot, type, settings)) {
|
|
83
|
+
const cached = await readCacheBlobs(config.srcArtDir, id);
|
|
84
|
+
if (cached) {
|
|
85
|
+
for (const [key, data] of cached) {
|
|
86
|
+
if (key === `${id}:__metadata__`) {
|
|
87
|
+
try {
|
|
88
|
+
asset.metadata = JSON.parse(new TextDecoder().decode(data));
|
|
89
|
+
}
|
|
90
|
+
catch { /* skip */ }
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
BlobStore.setProcessed(key, data);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
asset.status = 'ready';
|
|
97
|
+
asset.processedSize = BlobStore.getProcessed(id)?.length ?? 0;
|
|
98
|
+
if (verbose)
|
|
99
|
+
console.log(` [cached] ${id}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
assets.push(asset);
|
|
104
|
+
assetsById.set(id, asset);
|
|
105
|
+
}
|
|
106
|
+
// Materials from .stowmat files
|
|
107
|
+
for (const matFile of scan.matFiles) {
|
|
108
|
+
const id = matFile.relativePath;
|
|
109
|
+
const mat = await readStowmat(config.srcArtDir, id);
|
|
110
|
+
if (!mat)
|
|
111
|
+
continue;
|
|
112
|
+
const materialConfig = stowmatToMaterialConfig(mat);
|
|
113
|
+
const fileName = id.split('/').pop() ?? id;
|
|
114
|
+
const baseName = fileName.replace(/\.[^.]+$/, '');
|
|
115
|
+
let meta = await readStowmeta(config.srcArtDir, id);
|
|
116
|
+
if (!meta) {
|
|
117
|
+
meta = generateDefaultStowmeta(id, AssetType.MaterialSchema);
|
|
118
|
+
await writeStowmeta(config.srcArtDir, id, meta);
|
|
119
|
+
}
|
|
120
|
+
const asset = {
|
|
121
|
+
id,
|
|
122
|
+
fileName,
|
|
123
|
+
stringId: meta.stringId || baseName,
|
|
124
|
+
type: AssetType.MaterialSchema,
|
|
125
|
+
status: 'ready',
|
|
126
|
+
settings: { ...defaultAssetSettings(), materialConfig, pack: meta.pack ?? 'default', tags: meta.tags ?? [] },
|
|
127
|
+
sourceSize: matFile.size,
|
|
128
|
+
processedSize: 0,
|
|
129
|
+
};
|
|
130
|
+
assets.push(asset);
|
|
131
|
+
assetsById.set(id, asset);
|
|
132
|
+
}
|
|
133
|
+
// 3. Process stale assets
|
|
134
|
+
const pending = assets.filter(a => a.status === 'pending');
|
|
135
|
+
if (pending.length === 0) {
|
|
136
|
+
if (verbose)
|
|
137
|
+
console.log('All assets cached, nothing to process.');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(`Processing ${pending.length} asset(s)...`);
|
|
141
|
+
// Initialize encoders
|
|
142
|
+
const ctx = await initializeEncoders(opts?.wasmDir);
|
|
143
|
+
// Process with concurrency limit
|
|
144
|
+
let processed = 0;
|
|
145
|
+
const queue = [...pending];
|
|
146
|
+
async function processNext() {
|
|
147
|
+
while (queue.length > 0) {
|
|
148
|
+
const asset = queue.shift();
|
|
149
|
+
const id = asset.id;
|
|
150
|
+
try {
|
|
151
|
+
// Load source
|
|
152
|
+
const sourceData = await readFile(config.srcArtDir, id);
|
|
153
|
+
if (!sourceData)
|
|
154
|
+
throw new Error(`Could not read source file: ${id}`);
|
|
155
|
+
BlobStore.setSource(id, sourceData);
|
|
156
|
+
const t0 = performance.now();
|
|
157
|
+
const result = await processAsset(id, sourceData, asset.type, asset.stringId, asset.settings, ctx);
|
|
158
|
+
const elapsed = (performance.now() - t0).toFixed(0);
|
|
159
|
+
asset.status = 'ready';
|
|
160
|
+
asset.metadata = result.metadata;
|
|
161
|
+
asset.processedSize = result.processedSize;
|
|
162
|
+
processed++;
|
|
163
|
+
console.log(` [${processed}/${pending.length}] ${id} (${elapsed}ms)`);
|
|
164
|
+
// Write cache
|
|
165
|
+
const cacheEntries = new Map();
|
|
166
|
+
const processedBlob = BlobStore.getProcessed(id);
|
|
167
|
+
if (processedBlob)
|
|
168
|
+
cacheEntries.set(id, processedBlob);
|
|
169
|
+
if (result.metadata) {
|
|
170
|
+
cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(result.metadata)));
|
|
171
|
+
}
|
|
172
|
+
// Auxiliary blobs
|
|
173
|
+
for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
|
|
174
|
+
const blob = BlobStore.getProcessed(`${id}${suffix}`);
|
|
175
|
+
if (blob)
|
|
176
|
+
cacheEntries.set(`${id}${suffix}`, blob);
|
|
177
|
+
}
|
|
178
|
+
const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
|
|
179
|
+
const clipCount = animCountBlob ? animCountBlob[0] : 0;
|
|
180
|
+
for (let ci = 0; ci < clipCount; ci++) {
|
|
181
|
+
const animData = BlobStore.getProcessed(`${id}:anim:${ci}`);
|
|
182
|
+
if (animData)
|
|
183
|
+
cacheEntries.set(`${id}:anim:${ci}`, animData);
|
|
184
|
+
const animMeta = BlobStore.getProcessed(`${id}:animMeta:${ci}`);
|
|
185
|
+
if (animMeta)
|
|
186
|
+
cacheEntries.set(`${id}:animMeta:${ci}`, animMeta);
|
|
187
|
+
}
|
|
188
|
+
if (cacheEntries.size > 0) {
|
|
189
|
+
await writeCacheBlobs(config.srcArtDir, id, cacheEntries);
|
|
190
|
+
// Stamp cache in .stowmeta
|
|
191
|
+
const snapshot = await getFileSnapshot(config.srcArtDir, id);
|
|
192
|
+
if (snapshot) {
|
|
193
|
+
const meta = await readStowmeta(config.srcArtDir, id);
|
|
194
|
+
if (meta) {
|
|
195
|
+
meta.cache = buildCacheStamp(snapshot, asset.type, asset.settings);
|
|
196
|
+
await writeStowmeta(config.srcArtDir, id, meta);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
asset.status = 'error';
|
|
203
|
+
asset.error = err instanceof Error ? err.message : String(err);
|
|
204
|
+
processed++;
|
|
205
|
+
console.error(` [${processed}/${pending.length}] ${id} FAILED: ${asset.error}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const workers = [];
|
|
210
|
+
for (let i = 0; i < Math.min(maxConcurrent, pending.length); i++) {
|
|
211
|
+
workers.push(processNext());
|
|
212
|
+
}
|
|
213
|
+
await Promise.all(workers);
|
|
214
|
+
}
|
|
215
|
+
// 4. Clean orphaned caches/metas
|
|
216
|
+
await cleanupProject(config.projectDir, { verbose });
|
|
217
|
+
// 5. Build packs
|
|
218
|
+
const packs = config.config.packs ?? [{ name: 'default' }];
|
|
219
|
+
const cdnDir = path.resolve(config.projectDir, config.config.cdnAssetsPath ?? 'public/cdn-assets');
|
|
220
|
+
await fs.mkdir(cdnDir, { recursive: true });
|
|
221
|
+
for (const pack of packs) {
|
|
222
|
+
const packAssets = assets.filter(a => a.status === 'ready' &&
|
|
223
|
+
(a.settings.pack === pack.name || (!a.settings.pack && pack.name === 'default')));
|
|
224
|
+
if (packAssets.length === 0) {
|
|
225
|
+
if (verbose)
|
|
226
|
+
console.log(`Pack "${pack.name}": no assets, skipping`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
console.log(`Building pack "${pack.name}" (${packAssets.length} assets)...`);
|
|
230
|
+
const packData = buildPack(packAssets, assetsById);
|
|
231
|
+
const outPath = path.join(cdnDir, `${pack.name}.stow`);
|
|
232
|
+
await fs.writeFile(outPath, packData);
|
|
233
|
+
console.log(` → ${outPath} (${(packData.length / 1024).toFixed(0)} KB)`);
|
|
234
|
+
}
|
|
235
|
+
// Summary
|
|
236
|
+
const errors = assets.filter(a => a.status === 'error');
|
|
237
|
+
if (errors.length > 0) {
|
|
238
|
+
console.log(`\n${errors.length} asset(s) failed:`);
|
|
239
|
+
for (const e of errors)
|
|
240
|
+
console.log(` ${e.id}: ${e.error}`);
|
|
241
|
+
}
|
|
242
|
+
console.log('Build complete.');
|
|
243
|
+
}
|
|
244
|
+
// ─── Status ──────────────────────────────────────────────────────────────────
|
|
245
|
+
export async function showStatus(projectDir) {
|
|
246
|
+
const config = await readProjectConfig(projectDir);
|
|
247
|
+
const scan = await scanDirectory(config.srcArtDir);
|
|
248
|
+
console.log(`Project: ${config.projectName}`);
|
|
249
|
+
console.log(`Source dir: ${config.srcArtDir}`);
|
|
250
|
+
console.log(`Source files: ${scan.sourceFiles.length}`);
|
|
251
|
+
console.log(`Material files: ${scan.matFiles.length}`);
|
|
252
|
+
console.log(`Meta files: ${scan.metaFiles.length}`);
|
|
253
|
+
const packs = config.config.packs ?? [{ name: 'default' }];
|
|
254
|
+
console.log(`Packs: ${packs.map(p => p.name).join(', ')}`);
|
|
255
|
+
// Count stale
|
|
256
|
+
let cached = 0;
|
|
257
|
+
let stale = 0;
|
|
258
|
+
for (const file of scan.sourceFiles) {
|
|
259
|
+
const meta = await readStowmeta(config.srcArtDir, file.relativePath);
|
|
260
|
+
if (!meta) {
|
|
261
|
+
stale++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const { type, settings } = stowmetaToAssetSettings(meta);
|
|
265
|
+
if (meta.cache && isCacheValid(meta, file, type, settings)) {
|
|
266
|
+
cached++;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
stale++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(`Cached: ${cached}, Needs processing: ${stale}`);
|
|
273
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AssetType } from './core/types.js';
|
|
2
|
+
import type { AnimationClipMetadata } from './core/types.js';
|
|
3
|
+
import type { AssetSettings, ProjectAsset } from './app/state.js';
|
|
4
|
+
import type { ITextureEncoder, IMeshEncoder, IMeshImporter, IImageDecoder, IAudioDecoder, IAacEncoder, ImportedMesh } from './encoders/interfaces.js';
|
|
5
|
+
export interface ProcessingContext {
|
|
6
|
+
textureEncoder: ITextureEncoder;
|
|
7
|
+
meshEncoder: IMeshEncoder;
|
|
8
|
+
meshImporter: IMeshImporter;
|
|
9
|
+
imageDecoder: IImageDecoder;
|
|
10
|
+
audioDecoder: IAudioDecoder;
|
|
11
|
+
aacEncoder: IAacEncoder;
|
|
12
|
+
onProgress?: (id: string, msg: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ProcessResult {
|
|
15
|
+
metadata?: ProjectAsset['metadata'];
|
|
16
|
+
processedSize: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function processAsset(id: string, sourceData: Uint8Array, type: AssetType, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
19
|
+
export declare function buildPack(assets: ProjectAsset[], assetsById: Map<string, ProjectAsset>): Uint8Array;
|
|
20
|
+
export declare function buildAnimationDataBlobsV2(imported: ImportedMesh): {
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
metadata: AnimationClipMetadata;
|
|
23
|
+
}[];
|