@series-inc/stowkit-cli 0.6.39 → 0.6.41
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/ai-tagger.d.ts +10 -0
- package/dist/ai-tagger.js +63 -3
- package/dist/app/disk-project.d.ts +12 -1
- package/dist/app/state.d.ts +10 -2
- package/dist/app/state.js +1 -0
- package/dist/app/stowmeta-io.js +9 -0
- package/dist/app/stowspritesheet-io.d.ts +6 -0
- package/dist/app/stowspritesheet-io.js +40 -0
- package/dist/app/thumbnail-cache.js +1 -1
- package/dist/cleanup.js +7 -6
- package/dist/cli.js +2 -0
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +2 -0
- package/dist/core/types.d.ts +10 -1
- package/dist/core/types.js +1 -0
- package/dist/format/metadata.d.ts +3 -1
- package/dist/format/metadata.js +23 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/node-fs.d.ts +1 -0
- package/dist/node-fs.js +11 -4
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.js +32 -2
- package/dist/pipeline.js +37 -1
- package/dist/server.js +507 -1
- package/package.json +2 -2
package/dist/ai-tagger.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { AssetType } from './core/types.js';
|
|
2
|
+
export interface SpriteSheetDetection {
|
|
3
|
+
isSpriteSheet: boolean;
|
|
4
|
+
rows?: number;
|
|
5
|
+
columns?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Use Gemini Vision to detect whether an image is a sprite sheet.
|
|
9
|
+
* If it is, returns the detected row/column grid dimensions.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectSpriteSheet(apiKey: string, imageData: Buffer, mimeType: string): Promise<SpriteSheetDetection>;
|
|
2
12
|
/**
|
|
3
13
|
* Verify that a Gemini API key is valid by making a minimal request.
|
|
4
14
|
*/
|
package/dist/ai-tagger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GoogleGenAI } from '@google/genai';
|
|
2
2
|
import { AssetType } from './core/types.js';
|
|
3
|
-
const
|
|
3
|
+
const MODEL_LITE = 'gemini-3.1-flash-lite-preview';
|
|
4
|
+
const MODEL_FLASH = 'gemini-3.1-pro-preview';
|
|
4
5
|
const ASSET_TYPE_LABELS = {
|
|
5
6
|
[AssetType.Texture2D]: 'texture',
|
|
6
7
|
[AssetType.StaticMesh]: '3D mesh',
|
|
@@ -26,6 +27,65 @@ Rules:
|
|
|
26
27
|
- no generic tags like asset, file, media, content
|
|
27
28
|
- only include tags directly supported by the input`;
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Use Gemini Vision to detect whether an image is a sprite sheet.
|
|
32
|
+
* If it is, returns the detected row/column grid dimensions.
|
|
33
|
+
*/
|
|
34
|
+
export async function detectSpriteSheet(apiKey, imageData, mimeType) {
|
|
35
|
+
const client = new GoogleGenAI({ apiKey });
|
|
36
|
+
const response = await client.models.generateContent({
|
|
37
|
+
model: MODEL_FLASH,
|
|
38
|
+
contents: [{
|
|
39
|
+
role: 'user',
|
|
40
|
+
parts: [
|
|
41
|
+
{
|
|
42
|
+
text: `Analyze this image and determine if it is a sprite sheet: a grid of multiple animation frames or game sprites arranged in regular rows and columns.
|
|
43
|
+
|
|
44
|
+
If it IS a sprite sheet, count the rows and columns of the grid.
|
|
45
|
+
If it is NOT a sprite sheet (e.g. a single image, photo, icon, or seamless texture), respond accordingly.
|
|
46
|
+
|
|
47
|
+
Respond with ONLY a JSON object:
|
|
48
|
+
- Sprite sheet: { "isSpriteSheet": true, "rows": <number>, "columns": <number> }
|
|
49
|
+
- Not a sprite sheet: { "isSpriteSheet": false }`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
inlineData: {
|
|
53
|
+
data: imageData.toString('base64'),
|
|
54
|
+
mimeType,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}],
|
|
59
|
+
config: {
|
|
60
|
+
temperature: 0.1,
|
|
61
|
+
maxOutputTokens: 64,
|
|
62
|
+
responseMimeType: 'application/json',
|
|
63
|
+
responseSchema: {
|
|
64
|
+
type: 'OBJECT',
|
|
65
|
+
properties: {
|
|
66
|
+
isSpriteSheet: { type: 'BOOLEAN' },
|
|
67
|
+
rows: { type: 'INTEGER' },
|
|
68
|
+
columns: { type: 'INTEGER' },
|
|
69
|
+
},
|
|
70
|
+
required: ['isSpriteSheet'],
|
|
71
|
+
},
|
|
72
|
+
safetySettings: [
|
|
73
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'OFF' },
|
|
74
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'OFF' },
|
|
75
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'OFF' },
|
|
76
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'OFF' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const text = response.text ?? '{}';
|
|
81
|
+
const parsed = JSON.parse(text);
|
|
82
|
+
return {
|
|
83
|
+
isSpriteSheet: !!parsed.isSpriteSheet,
|
|
84
|
+
rows: typeof parsed.rows === 'number' && parsed.rows > 0 ? parsed.rows : undefined,
|
|
85
|
+
columns: typeof parsed.columns === 'number' && parsed.columns > 0 ? parsed.columns : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ─── API Key Verification ─────────────────────────────────────────────────────
|
|
29
89
|
/**
|
|
30
90
|
* Verify that a Gemini API key is valid by making a minimal request.
|
|
31
91
|
*/
|
|
@@ -33,7 +93,7 @@ export async function verifyApiKey(apiKey) {
|
|
|
33
93
|
try {
|
|
34
94
|
const client = new GoogleGenAI({ apiKey });
|
|
35
95
|
const response = await client.models.generateContent({
|
|
36
|
-
model:
|
|
96
|
+
model: MODEL_LITE,
|
|
37
97
|
contents: [{ role: 'user', parts: [{ text: 'Respond with the word "ok"' }] }],
|
|
38
98
|
config: { maxOutputTokens: 10 },
|
|
39
99
|
});
|
|
@@ -51,7 +111,7 @@ export async function verifyApiKey(apiKey) {
|
|
|
51
111
|
export async function tagAsset(apiKey, assetType, fileName, inputData, mimeType) {
|
|
52
112
|
const client = new GoogleGenAI({ apiKey });
|
|
53
113
|
const response = await client.models.generateContent({
|
|
54
|
-
model:
|
|
114
|
+
model: MODEL_LITE,
|
|
55
115
|
contents: [{
|
|
56
116
|
role: 'user',
|
|
57
117
|
parts: [
|
|
@@ -80,6 +80,9 @@ export interface StowMetaAnimationClip extends StowMetaBase {
|
|
|
80
80
|
export interface StowMetaMaterialSchema extends StowMetaBase {
|
|
81
81
|
type: 'materialSchema';
|
|
82
82
|
}
|
|
83
|
+
export interface StowMetaSpriteSheet extends StowMetaBase {
|
|
84
|
+
type: 'spriteSheet';
|
|
85
|
+
}
|
|
83
86
|
export interface StowMetaGlbChild {
|
|
84
87
|
name: string;
|
|
85
88
|
childType: string;
|
|
@@ -108,7 +111,7 @@ export interface StowMetaGlbContainer extends StowMetaBase {
|
|
|
108
111
|
preserveHierarchy?: boolean;
|
|
109
112
|
children: StowMetaGlbChild[];
|
|
110
113
|
}
|
|
111
|
-
export type StowMeta = StowMetaTexture | StowMetaAudio | StowMetaStaticMesh | StowMetaSkinnedMesh | StowMetaAnimationClip | StowMetaMaterialSchema | StowMetaGlbContainer;
|
|
114
|
+
export type StowMeta = StowMetaTexture | StowMetaAudio | StowMetaStaticMesh | StowMetaSkinnedMesh | StowMetaAnimationClip | StowMetaMaterialSchema | StowMetaSpriteSheet | StowMetaGlbContainer;
|
|
112
115
|
export interface StowMatProperty {
|
|
113
116
|
fieldName: string;
|
|
114
117
|
fieldType: string;
|
|
@@ -121,6 +124,14 @@ export interface StowMat {
|
|
|
121
124
|
schemaName: string;
|
|
122
125
|
properties: StowMatProperty[];
|
|
123
126
|
}
|
|
127
|
+
export interface StowSpriteSheet {
|
|
128
|
+
version: 1;
|
|
129
|
+
textureAsset: string | null;
|
|
130
|
+
rows: number;
|
|
131
|
+
columns: number;
|
|
132
|
+
frameCount: number;
|
|
133
|
+
frameRate: number;
|
|
134
|
+
}
|
|
124
135
|
export declare const KTX2_QUALITY_STRINGS: Record<string, KTX2Quality>;
|
|
125
136
|
export declare const KTX2_QUALITY_TO_STRING: Record<number, string>;
|
|
126
137
|
export declare const TEXTURE_RESIZE_STRINGS: Record<string, TextureResize>;
|
package/dist/app/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetType, TextureMetadata, AudioMetadata, MeshMetadata, AnimationClipMetadata, SkinnedMeshMetadata, KTX2Quality, TextureResize, TextureFilterMode, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
1
|
+
import type { AssetType, TextureMetadata, AudioMetadata, MeshMetadata, AnimationClipMetadata, SkinnedMeshMetadata, SpriteSheetMetadata, KTX2Quality, TextureResize, TextureFilterMode, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
2
2
|
export interface MaterialProperty {
|
|
3
3
|
fieldName: string;
|
|
4
4
|
fieldType: MaterialFieldType;
|
|
@@ -10,6 +10,13 @@ export interface MaterialConfig {
|
|
|
10
10
|
schemaId: string;
|
|
11
11
|
properties: MaterialProperty[];
|
|
12
12
|
}
|
|
13
|
+
export interface SpriteSheetConfig {
|
|
14
|
+
textureAssetId: string | null;
|
|
15
|
+
rows: number;
|
|
16
|
+
columns: number;
|
|
17
|
+
frameCount: number;
|
|
18
|
+
frameRate: number;
|
|
19
|
+
}
|
|
13
20
|
export interface AssetSettings {
|
|
14
21
|
quality: KTX2Quality;
|
|
15
22
|
resize: TextureResize;
|
|
@@ -21,6 +28,7 @@ export interface AssetSettings {
|
|
|
21
28
|
audioSampleRate: AudioSampleRate;
|
|
22
29
|
targetMeshId: string | null;
|
|
23
30
|
materialConfig: MaterialConfig;
|
|
31
|
+
spritesheetConfig: SpriteSheetConfig;
|
|
24
32
|
materialOverrides: Record<number, string | null>;
|
|
25
33
|
pack: string;
|
|
26
34
|
excluded: boolean;
|
|
@@ -35,7 +43,7 @@ export interface ProjectAsset {
|
|
|
35
43
|
status: 'loading' | 'pending' | 'processing' | 'ready' | 'error';
|
|
36
44
|
error?: string;
|
|
37
45
|
settings: AssetSettings;
|
|
38
|
-
metadata?: TextureMetadata | AudioMetadata | MeshMetadata | AnimationClipMetadata | SkinnedMeshMetadata;
|
|
46
|
+
metadata?: TextureMetadata | AudioMetadata | MeshMetadata | AnimationClipMetadata | SkinnedMeshMetadata | SpriteSheetMetadata;
|
|
39
47
|
sourceSize: number;
|
|
40
48
|
processedSize: number;
|
|
41
49
|
parentId?: string;
|
package/dist/app/state.js
CHANGED
|
@@ -11,6 +11,7 @@ export function defaultAssetSettings() {
|
|
|
11
11
|
audioSampleRate: ASR.Auto,
|
|
12
12
|
targetMeshId: null,
|
|
13
13
|
materialConfig: { schemaId: '', properties: [] },
|
|
14
|
+
spritesheetConfig: { textureAssetId: null, rows: 1, columns: 1, frameCount: 1, frameRate: 12 },
|
|
14
15
|
materialOverrides: {},
|
|
15
16
|
pack: 'default',
|
|
16
17
|
excluded: false,
|
package/dist/app/stowmeta-io.js
CHANGED
|
@@ -114,6 +114,11 @@ export function stowmetaToAssetSettings(meta) {
|
|
|
114
114
|
type: AssetType.MaterialSchema,
|
|
115
115
|
settings: base,
|
|
116
116
|
};
|
|
117
|
+
case 'spriteSheet':
|
|
118
|
+
return {
|
|
119
|
+
type: AssetType.SpriteSheet,
|
|
120
|
+
settings: base,
|
|
121
|
+
};
|
|
117
122
|
case 'glbContainer':
|
|
118
123
|
return {
|
|
119
124
|
type: AssetType.GlbContainer,
|
|
@@ -185,6 +190,8 @@ export function assetSettingsToStowmeta(asset) {
|
|
|
185
190
|
};
|
|
186
191
|
case AssetType.MaterialSchema:
|
|
187
192
|
return { ...base, type: 'materialSchema' };
|
|
193
|
+
case AssetType.SpriteSheet:
|
|
194
|
+
return { ...base, type: 'spriteSheet' };
|
|
188
195
|
case AssetType.GlbContainer:
|
|
189
196
|
return { ...base, type: 'glbContainer', preserveHierarchy: asset.settings.preserveHierarchy || undefined, children: [] };
|
|
190
197
|
default:
|
|
@@ -250,6 +257,8 @@ export function generateDefaultStowmeta(relativePath, type, projectDefaults, ima
|
|
|
250
257
|
return { ...base, type: 'animationClip', targetMeshId: null };
|
|
251
258
|
case AssetType.MaterialSchema:
|
|
252
259
|
return { ...base, type: 'materialSchema' };
|
|
260
|
+
case AssetType.SpriteSheet:
|
|
261
|
+
return { ...base, type: 'spriteSheet' };
|
|
253
262
|
case AssetType.GlbContainer:
|
|
254
263
|
return { ...base, type: 'glbContainer', children: [] };
|
|
255
264
|
default:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SpriteSheetConfig } from './state.js';
|
|
2
|
+
import type { StowSpriteSheet } from './disk-project.js';
|
|
3
|
+
export declare function readStowSpriteSheet(srcArtDir: string, relativePath: string): Promise<StowSpriteSheet | null>;
|
|
4
|
+
export declare function writeStowSpriteSheet(srcArtDir: string, relativePath: string, sheet: StowSpriteSheet): Promise<void>;
|
|
5
|
+
export declare function spritesheetConfigToStowSpriteSheet(config: SpriteSheetConfig): StowSpriteSheet;
|
|
6
|
+
export declare function stowSpriteSheetToConfig(sheet: StowSpriteSheet): SpriteSheetConfig;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readTextFile, writeFile } from '../node-fs.js';
|
|
2
|
+
export async function readStowSpriteSheet(srcArtDir, relativePath) {
|
|
3
|
+
const text = await readTextFile(srcArtDir, relativePath);
|
|
4
|
+
if (!text)
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(text);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function writeStowSpriteSheet(srcArtDir, relativePath, sheet) {
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.stringify(sheet, null, 2);
|
|
16
|
+
await writeFile(srcArtDir, relativePath, json);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.warn(`[stowspritesheet] Failed to write ${relativePath}:`, err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function spritesheetConfigToStowSpriteSheet(config) {
|
|
23
|
+
return {
|
|
24
|
+
version: 1,
|
|
25
|
+
textureAsset: config.textureAssetId,
|
|
26
|
+
rows: config.rows,
|
|
27
|
+
columns: config.columns,
|
|
28
|
+
frameCount: config.frameCount,
|
|
29
|
+
frameRate: config.frameRate,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function stowSpriteSheetToConfig(sheet) {
|
|
33
|
+
return {
|
|
34
|
+
textureAssetId: sheet.textureAsset ?? null,
|
|
35
|
+
rows: sheet.rows ?? 1,
|
|
36
|
+
columns: sheet.columns ?? 1,
|
|
37
|
+
frameCount: sheet.frameCount ?? 1,
|
|
38
|
+
frameRate: sheet.frameRate ?? 12,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -36,7 +36,7 @@ export async function writeManifest(srcArtDir, manifest) {
|
|
|
36
36
|
export function computeSettingsHash(settings) {
|
|
37
37
|
// Only include settings that affect visual output
|
|
38
38
|
const relevant = {};
|
|
39
|
-
for (const key of ['quality', 'resize', 'generateMipmaps', 'dracoQuality', 'materialConfig', 'targetMeshId', 'materialOverrides']) {
|
|
39
|
+
for (const key of ['quality', 'resize', 'generateMipmaps', 'dracoQuality', 'materialConfig', 'spritesheetConfig', 'targetMeshId', 'materialOverrides']) {
|
|
40
40
|
if (settings[key] !== undefined)
|
|
41
41
|
relevant[key] = settings[key];
|
|
42
42
|
}
|
package/dist/cleanup.js
CHANGED
|
@@ -9,11 +9,12 @@ export async function cleanupProject(projectDir, opts) {
|
|
|
9
9
|
// Build set of source files that exist
|
|
10
10
|
const sourceFiles = new Set(scan.sourceFiles.map(f => f.relativePath));
|
|
11
11
|
const matFiles = new Set(scan.matFiles.map(f => f.relativePath));
|
|
12
|
+
const spritesheetFiles = new Set(scan.spritesheetFiles.map(f => f.relativePath));
|
|
12
13
|
let deletedCaches = 0;
|
|
13
14
|
let deletedMetas = 0;
|
|
14
15
|
let freedBytes = 0;
|
|
15
16
|
// Walk all files looking for orphaned .stowcache and .stowmeta
|
|
16
|
-
await walkForOrphans(config.srcArtDir, '', sourceFiles, matFiles, async (orphanPath, type, size) => {
|
|
17
|
+
await walkForOrphans(config.srcArtDir, '', sourceFiles, matFiles, spritesheetFiles, async (orphanPath, type, size) => {
|
|
17
18
|
const fullPath = path.join(config.srcArtDir, orphanPath);
|
|
18
19
|
await fs.unlink(fullPath);
|
|
19
20
|
freedBytes += size;
|
|
@@ -43,7 +44,7 @@ export async function cleanupProject(projectDir, opts) {
|
|
|
43
44
|
console.log(`Cleaned up ${deletedCaches} cache(s) and ${deletedMetas} meta(s)${thumbNote} (${(freedBytes / 1024).toFixed(0)} KB freed)`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
async function walkForOrphans(basePath, prefix, sourceFiles, matFiles, onOrphan) {
|
|
47
|
+
async function walkForOrphans(basePath, prefix, sourceFiles, matFiles, spritesheetFiles, onOrphan) {
|
|
47
48
|
const dirPath = prefix ? path.join(basePath, prefix) : basePath;
|
|
48
49
|
let entries;
|
|
49
50
|
try {
|
|
@@ -59,21 +60,21 @@ async function walkForOrphans(basePath, prefix, sourceFiles, matFiles, onOrphan)
|
|
|
59
60
|
continue;
|
|
60
61
|
if (entry.name.endsWith('.children'))
|
|
61
62
|
continue;
|
|
62
|
-
await walkForOrphans(basePath, relativePath, sourceFiles, matFiles, onOrphan);
|
|
63
|
+
await walkForOrphans(basePath, relativePath, sourceFiles, matFiles, spritesheetFiles, onOrphan);
|
|
63
64
|
}
|
|
64
65
|
else if (entry.isFile()) {
|
|
65
66
|
if (entry.name.endsWith('.stowcache')) {
|
|
66
67
|
// Cache orphan: source file doesn't exist
|
|
67
68
|
const sourceId = relativePath.replace(/\.stowcache$/, '');
|
|
68
|
-
if (!sourceFiles.has(sourceId) && !matFiles.has(sourceId)) {
|
|
69
|
+
if (!sourceFiles.has(sourceId) && !matFiles.has(sourceId) && !spritesheetFiles.has(sourceId)) {
|
|
69
70
|
const stat = await fs.stat(path.join(basePath, relativePath));
|
|
70
71
|
await onOrphan(relativePath, 'cache', stat.size);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
else if (entry.name.endsWith('.stowmeta')) {
|
|
74
|
-
// Meta orphan: neither source file nor
|
|
75
|
+
// Meta orphan: neither source file, mat file, nor spritesheet file exists
|
|
75
76
|
const sourceId = relativePath.replace(/\.stowmeta$/, '');
|
|
76
|
-
if (!sourceFiles.has(sourceId) && !matFiles.has(sourceId)) {
|
|
77
|
+
if (!sourceFiles.has(sourceId) && !matFiles.has(sourceId) && !spritesheetFiles.has(sourceId)) {
|
|
77
78
|
const stat = await fs.stat(path.join(basePath, relativePath));
|
|
78
79
|
await onOrphan(relativePath, 'meta', stat.size);
|
|
79
80
|
}
|
package/dist/cli.js
CHANGED
|
@@ -78,6 +78,7 @@ Usage:
|
|
|
78
78
|
stowkit init [dir] Initialize a StowKit project (interactive menu)
|
|
79
79
|
stowkit init --with-engine Initialize with 3D engine included
|
|
80
80
|
stowkit init --no-engine Initialize without 3D engine (skip prompt)
|
|
81
|
+
stowkit init --with-publish Initialize with assets-package.json for publishing
|
|
81
82
|
stowkit init --update [dir] Update AI skill files to match installed CLI version
|
|
82
83
|
stowkit build [dir] Full build: scan + process + pack
|
|
83
84
|
stowkit scan [dir] Detect new assets, generate .stowmeta defaults
|
|
@@ -175,6 +176,7 @@ async function main() {
|
|
|
175
176
|
update: args.includes('--update'),
|
|
176
177
|
withEngine: args.includes('--with-engine'),
|
|
177
178
|
noEngine: args.includes('--no-engine'),
|
|
179
|
+
withPublish: args.includes('--with-publish'),
|
|
178
180
|
});
|
|
179
181
|
break;
|
|
180
182
|
case 'update': {
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -69,3 +69,5 @@ export declare const ANIMATION_CLIP_METADATA_FIXED_SIZE = 268;
|
|
|
69
69
|
export declare const TRACK_NAME_SIZE = 64;
|
|
70
70
|
/** Metadata version discriminator for v2 animation clips */
|
|
71
71
|
export declare const ANIMATION_METADATA_VERSION = 2;
|
|
72
|
+
/** Size of SpriteSheetMetadata on disk (bytes) */
|
|
73
|
+
export declare const SPRITESHEET_METADATA_SIZE = 272;
|
package/dist/core/constants.js
CHANGED
|
@@ -70,3 +70,5 @@ export const ANIMATION_CLIP_METADATA_FIXED_SIZE = 268;
|
|
|
70
70
|
export const TRACK_NAME_SIZE = 64;
|
|
71
71
|
/** Metadata version discriminator for v2 animation clips */
|
|
72
72
|
export const ANIMATION_METADATA_VERSION = 2;
|
|
73
|
+
/** Size of SpriteSheetMetadata on disk (bytes) */
|
|
74
|
+
export const SPRITESHEET_METADATA_SIZE = 272;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare enum AssetType {
|
|
|
6
6
|
MaterialSchema = 4,
|
|
7
7
|
SkinnedMesh = 5,
|
|
8
8
|
AnimationClip = 6,
|
|
9
|
-
GlbContainer = 7
|
|
9
|
+
GlbContainer = 7,
|
|
10
|
+
SpriteSheet = 8
|
|
10
11
|
}
|
|
11
12
|
export declare enum KTX2Quality {
|
|
12
13
|
Fastest = 0,
|
|
@@ -142,6 +143,14 @@ export interface MaterialSchemaMetadata {
|
|
|
142
143
|
fieldCount: number;
|
|
143
144
|
fields: MaterialSchemaField[];
|
|
144
145
|
}
|
|
146
|
+
export interface SpriteSheetMetadata {
|
|
147
|
+
stringId: string;
|
|
148
|
+
textureId: string;
|
|
149
|
+
rows: number;
|
|
150
|
+
columns: number;
|
|
151
|
+
frameCount: number;
|
|
152
|
+
frameRate: number;
|
|
153
|
+
}
|
|
145
154
|
export interface MeshMetadata {
|
|
146
155
|
meshGeometryCount: number;
|
|
147
156
|
materialCount: number;
|
package/dist/core/types.js
CHANGED
|
@@ -9,6 +9,7 @@ export var AssetType;
|
|
|
9
9
|
AssetType[AssetType["SkinnedMesh"] = 5] = "SkinnedMesh";
|
|
10
10
|
AssetType[AssetType["AnimationClip"] = 6] = "AnimationClip";
|
|
11
11
|
AssetType[AssetType["GlbContainer"] = 7] = "GlbContainer";
|
|
12
|
+
AssetType[AssetType["SpriteSheet"] = 8] = "SpriteSheet";
|
|
12
13
|
})(AssetType || (AssetType = {}));
|
|
13
14
|
// ─── Texture Enums ──────────────────────────────────────────────────────────
|
|
14
15
|
export var KTX2Quality;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TextureMetadata, AudioMetadata, MeshMetadata, MaterialSchemaMetadata, AnimationClipMetadata, SkinnedMeshMetadata } from '../core/types.js';
|
|
1
|
+
import type { TextureMetadata, AudioMetadata, MeshMetadata, MaterialSchemaMetadata, AnimationClipMetadata, SkinnedMeshMetadata, SpriteSheetMetadata } from '../core/types.js';
|
|
2
2
|
export declare function serializeTextureMetadata(meta: TextureMetadata): Uint8Array;
|
|
3
3
|
export declare function deserializeTextureMetadata(data: Uint8Array): TextureMetadata;
|
|
4
4
|
export declare function serializeAudioMetadata(meta: AudioMetadata): Uint8Array;
|
|
@@ -11,6 +11,8 @@ export declare function serializeAnimationClipMetadata(meta: AnimationClipMetada
|
|
|
11
11
|
export declare function deserializeAnimationClipMetadata(data: Uint8Array): AnimationClipMetadata;
|
|
12
12
|
export declare function serializeSkinnedMeshMetadata(meta: SkinnedMeshMetadata): Uint8Array;
|
|
13
13
|
export declare function deserializeSkinnedMeshMetadata(data: Uint8Array): SkinnedMeshMetadata;
|
|
14
|
+
export declare function serializeSpriteSheetMetadata(meta: SpriteSheetMetadata): Uint8Array;
|
|
15
|
+
export declare function deserializeSpriteSheetMetadata(data: Uint8Array): SpriteSheetMetadata;
|
|
14
16
|
export declare function wrapMetadata(assetMetadata: Uint8Array, tags: string[]): Uint8Array;
|
|
15
17
|
export declare function unwrapMetadata(wrapped: Uint8Array): {
|
|
16
18
|
tags: string[];
|
package/dist/format/metadata.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BinaryReader, BinaryWriter } from '../core/binary.js';
|
|
2
|
-
import { TEXTURE_METADATA_SIZE, AUDIO_METADATA_SIZE, STRING_ID_SIZE, MESH_GEOMETRY_INFO_SIZE, SCENE_NODE_SIZE, MATERIAL_DATA_FIXED_SIZE, MATERIAL_PROPERTY_VALUE_SIZE, MESH_METADATA_FIXED_SIZE, NODE_NAME_SIZE, MATERIAL_NAME_SIZE, MATERIAL_SCHEMA_ID_SIZE, MATERIAL_FIELD_NAME_SIZE, MATERIAL_SCHEMA_NAME_SIZE, MATERIAL_SCHEMA_DEFAULT_TEXTURE_ID_SIZE, MATERIAL_SCHEMA_METADATA_FIXED_SIZE, MATERIAL_SCHEMA_FIELD_SIZE, BONE_NAME_SIZE, SKINNED_MESH_GEOMETRY_INFO_SIZE, SKINNED_MESH_METADATA_FIXED_SIZE, BONE_SIZE, ANIMATION_TRACK_DESCRIPTOR_SIZE, ANIMATION_CLIP_METADATA_FIXED_SIZE, TRACK_NAME_SIZE, ANIMATION_METADATA_VERSION, } from '../core/constants.js';
|
|
2
|
+
import { TEXTURE_METADATA_SIZE, AUDIO_METADATA_SIZE, STRING_ID_SIZE, MESH_GEOMETRY_INFO_SIZE, SCENE_NODE_SIZE, MATERIAL_DATA_FIXED_SIZE, MATERIAL_PROPERTY_VALUE_SIZE, MESH_METADATA_FIXED_SIZE, NODE_NAME_SIZE, MATERIAL_NAME_SIZE, MATERIAL_SCHEMA_ID_SIZE, MATERIAL_FIELD_NAME_SIZE, MATERIAL_SCHEMA_NAME_SIZE, MATERIAL_SCHEMA_DEFAULT_TEXTURE_ID_SIZE, MATERIAL_SCHEMA_METADATA_FIXED_SIZE, MATERIAL_SCHEMA_FIELD_SIZE, BONE_NAME_SIZE, SKINNED_MESH_GEOMETRY_INFO_SIZE, SKINNED_MESH_METADATA_FIXED_SIZE, BONE_SIZE, ANIMATION_TRACK_DESCRIPTOR_SIZE, ANIMATION_CLIP_METADATA_FIXED_SIZE, TRACK_NAME_SIZE, ANIMATION_METADATA_VERSION, SPRITESHEET_METADATA_SIZE, } from '../core/constants.js';
|
|
3
3
|
import { TextureFilterMode } from '../core/types.js';
|
|
4
4
|
// ─── Texture Metadata ───────────────────────────────────────────────────────
|
|
5
5
|
export function serializeTextureMetadata(meta) {
|
|
@@ -361,6 +361,28 @@ export function deserializeSkinnedMeshMetadata(data) {
|
|
|
361
361
|
bones.push(deserializeBone(r));
|
|
362
362
|
return { meshGeometryCount, materialCount, nodeCount, boneCount, stringId, geometries, materials, nodes, meshIndices, bones };
|
|
363
363
|
}
|
|
364
|
+
// ─── SpriteSheet Metadata ────────────────────────────────────────────────────
|
|
365
|
+
export function serializeSpriteSheetMetadata(meta) {
|
|
366
|
+
const w = new BinaryWriter(SPRITESHEET_METADATA_SIZE);
|
|
367
|
+
w.writeFixedString(meta.stringId, STRING_ID_SIZE);
|
|
368
|
+
w.writeFixedString(meta.textureId, STRING_ID_SIZE);
|
|
369
|
+
w.writeUint32(meta.rows);
|
|
370
|
+
w.writeUint32(meta.columns);
|
|
371
|
+
w.writeUint32(meta.frameCount);
|
|
372
|
+
w.writeFloat32(meta.frameRate);
|
|
373
|
+
return w.getUint8Array();
|
|
374
|
+
}
|
|
375
|
+
export function deserializeSpriteSheetMetadata(data) {
|
|
376
|
+
const r = new BinaryReader(data);
|
|
377
|
+
return {
|
|
378
|
+
stringId: r.readFixedString(STRING_ID_SIZE),
|
|
379
|
+
textureId: r.readFixedString(STRING_ID_SIZE),
|
|
380
|
+
rows: r.readUint32(),
|
|
381
|
+
columns: r.readUint32(),
|
|
382
|
+
frameCount: r.readUint32(),
|
|
383
|
+
frameRate: r.readFloat32(),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
364
386
|
// ─── Metadata Wrapping ──────────────────────────────────────────────────────
|
|
365
387
|
export function wrapMetadata(assetMetadata, tags) {
|
|
366
388
|
const tagCsv = tags.length > 0 ? tags.join(',') + '\0' : '\0';
|
package/dist/init.d.ts
CHANGED
package/dist/init.js
CHANGED
|
@@ -153,6 +153,12 @@ export async function initProject(projectDir, opts) {
|
|
|
153
153
|
if (withEngine) {
|
|
154
154
|
await installEngine(absDir);
|
|
155
155
|
}
|
|
156
|
+
// Create assets-package.json for publishing
|
|
157
|
+
if (opts?.withPublish) {
|
|
158
|
+
const { initAssetsPackage } = await import('./assets-package.js');
|
|
159
|
+
await initAssetsPackage(absDir, { name: path.basename(absDir) });
|
|
160
|
+
console.log(` Publish config: assets-package.json`);
|
|
161
|
+
}
|
|
156
162
|
console.log('');
|
|
157
163
|
if (withEngine) {
|
|
158
164
|
console.log('Ready to go! Run:');
|
package/dist/node-fs.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ScanResult {
|
|
|
14
14
|
sourceFiles: FileSnapshot[];
|
|
15
15
|
metaFiles: FileSnapshot[];
|
|
16
16
|
matFiles: FileSnapshot[];
|
|
17
|
+
spritesheetFiles: FileSnapshot[];
|
|
17
18
|
folders: string[];
|
|
18
19
|
}
|
|
19
20
|
export declare function scanDirectory(basePath: string): Promise<ScanResult>;
|
package/dist/node-fs.js
CHANGED
|
@@ -98,15 +98,19 @@ function isMetaFile(name) {
|
|
|
98
98
|
function isMatFile(name) {
|
|
99
99
|
return name.endsWith('.stowmat');
|
|
100
100
|
}
|
|
101
|
+
function isSpritesheetFile(name) {
|
|
102
|
+
return name.endsWith('.stowspritesheet');
|
|
103
|
+
}
|
|
101
104
|
export async function scanDirectory(basePath) {
|
|
102
105
|
const sourceFiles = [];
|
|
103
106
|
const metaFiles = [];
|
|
104
107
|
const matFiles = [];
|
|
108
|
+
const spritesheetFiles = [];
|
|
105
109
|
const folders = [];
|
|
106
|
-
await walkDirectory(basePath, '', sourceFiles, metaFiles, matFiles, folders);
|
|
107
|
-
return { sourceFiles, metaFiles, matFiles, folders };
|
|
110
|
+
await walkDirectory(basePath, '', sourceFiles, metaFiles, matFiles, spritesheetFiles, folders);
|
|
111
|
+
return { sourceFiles, metaFiles, matFiles, spritesheetFiles, folders };
|
|
108
112
|
}
|
|
109
|
-
async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles, folders) {
|
|
113
|
+
async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles, spritesheetFiles, folders) {
|
|
110
114
|
const dirPath = prefix ? path.join(basePath, prefix) : basePath;
|
|
111
115
|
let entries;
|
|
112
116
|
try {
|
|
@@ -123,7 +127,7 @@ async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles,
|
|
|
123
127
|
if (entry.name.endsWith('.children'))
|
|
124
128
|
continue;
|
|
125
129
|
folders.push(relativePath);
|
|
126
|
-
await walkDirectory(basePath, relativePath, sourceFiles, metaFiles, matFiles, folders);
|
|
130
|
+
await walkDirectory(basePath, relativePath, sourceFiles, metaFiles, matFiles, spritesheetFiles, folders);
|
|
127
131
|
}
|
|
128
132
|
else if (entry.isFile()) {
|
|
129
133
|
const fullPath = path.join(basePath, relativePath);
|
|
@@ -145,6 +149,9 @@ async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles,
|
|
|
145
149
|
else if (isMatFile(entry.name)) {
|
|
146
150
|
matFiles.push(snapshot);
|
|
147
151
|
}
|
|
152
|
+
else if (isSpritesheetFile(entry.name)) {
|
|
153
|
+
spritesheetFiles.push(snapshot);
|
|
154
|
+
}
|
|
148
155
|
else if (isSourceFile(entry.name)) {
|
|
149
156
|
sourceFiles.push(snapshot);
|
|
150
157
|
}
|
package/dist/orchestrator.d.ts
CHANGED
package/dist/orchestrator.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readProjectConfig, scanDirectory, readFile, getFileSnapshot, probeImage
|
|
|
8
8
|
import { detectAssetType, readStowmeta, writeStowmeta, stowmetaToAssetSettings, generateDefaultStowmeta, glbChildToAssetSettings, generateDefaultGlbChild, } from './app/stowmeta-io.js';
|
|
9
9
|
import { parseGlb, pbrToMaterialConfig } from './encoders/glb-loader.js';
|
|
10
10
|
import { readStowmat, stowmatToMaterialConfig } from './app/stowmat-io.js';
|
|
11
|
+
import { readStowSpriteSheet, stowSpriteSheetToConfig } from './app/stowspritesheet-io.js';
|
|
11
12
|
import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, } from './app/process-cache.js';
|
|
12
13
|
import { buildPack, processExtractedAnimations, validatePackDependencies } from './pipeline.js';
|
|
13
14
|
import { WorkerPool } from './workers/worker-pool.js';
|
|
@@ -37,8 +38,9 @@ export async function scanProject(projectDir, opts) {
|
|
|
37
38
|
sourceFiles: scan.sourceFiles.length,
|
|
38
39
|
metaFiles: scan.metaFiles.length,
|
|
39
40
|
matFiles: scan.matFiles.length,
|
|
41
|
+
spritesheetFiles: scan.spritesheetFiles.length,
|
|
40
42
|
newFiles,
|
|
41
|
-
totalAssets: scan.sourceFiles.length + scan.matFiles.length,
|
|
43
|
+
totalAssets: scan.sourceFiles.length + scan.matFiles.length + scan.spritesheetFiles.length,
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
// ─── Full Build ──────────────────────────────────────────────────────────────
|
|
@@ -56,7 +58,7 @@ export async function fullBuild(projectDir, opts) {
|
|
|
56
58
|
// 1. Scan
|
|
57
59
|
const scan = await scanDirectory(config.srcArtDir);
|
|
58
60
|
if (verbose)
|
|
59
|
-
console.log(`Found ${scan.sourceFiles.length} source files, ${scan.matFiles.length} materials`);
|
|
61
|
+
console.log(`Found ${scan.sourceFiles.length} source files, ${scan.matFiles.length} materials, ${scan.spritesheetFiles.length} spritesheets`);
|
|
60
62
|
// 2. Build asset list
|
|
61
63
|
const assets = [];
|
|
62
64
|
const assetsById = new Map();
|
|
@@ -139,6 +141,33 @@ export async function fullBuild(projectDir, opts) {
|
|
|
139
141
|
assets.push(asset);
|
|
140
142
|
assetsById.set(id, asset);
|
|
141
143
|
}
|
|
144
|
+
// SpriteSheets from .stowspritesheet files
|
|
145
|
+
for (const ssFile of scan.spritesheetFiles) {
|
|
146
|
+
const id = ssFile.relativePath;
|
|
147
|
+
const sheet = await readStowSpriteSheet(config.srcArtDir, id);
|
|
148
|
+
if (!sheet)
|
|
149
|
+
continue;
|
|
150
|
+
const spritesheetConfig = stowSpriteSheetToConfig(sheet);
|
|
151
|
+
const fileName = id.split('/').pop() ?? id;
|
|
152
|
+
const baseName = fileName.replace(/\.[^.]+$/, '');
|
|
153
|
+
let meta = await readStowmeta(config.srcArtDir, id);
|
|
154
|
+
if (!meta) {
|
|
155
|
+
meta = generateDefaultStowmeta(id, AssetType.SpriteSheet, config.config.defaults);
|
|
156
|
+
await writeStowmeta(config.srcArtDir, id, meta);
|
|
157
|
+
}
|
|
158
|
+
const asset = {
|
|
159
|
+
id,
|
|
160
|
+
fileName,
|
|
161
|
+
stringId: meta.stringId || baseName,
|
|
162
|
+
type: AssetType.SpriteSheet,
|
|
163
|
+
status: 'ready',
|
|
164
|
+
settings: { ...defaultAssetSettings(), spritesheetConfig, pack: meta.pack ?? 'default', tags: meta.tags ?? [] },
|
|
165
|
+
sourceSize: ssFile.size,
|
|
166
|
+
processedSize: 0,
|
|
167
|
+
};
|
|
168
|
+
assets.push(asset);
|
|
169
|
+
assetsById.set(id, asset);
|
|
170
|
+
}
|
|
142
171
|
// 2b. Extract all GLB containers (always re-parse so preserveHierarchy etc. are reflected)
|
|
143
172
|
const glbExtracts = new Map();
|
|
144
173
|
const glbContainers = assets.filter(a => a.type === AssetType.GlbContainer);
|
|
@@ -480,6 +509,7 @@ export async function showStatus(projectDir) {
|
|
|
480
509
|
console.log(`Source dir: ${config.srcArtDir}`);
|
|
481
510
|
console.log(`Source files: ${scan.sourceFiles.length}`);
|
|
482
511
|
console.log(`Material files: ${scan.matFiles.length}`);
|
|
512
|
+
console.log(`SpriteSheet files: ${scan.spritesheetFiles.length}`);
|
|
483
513
|
console.log(`Meta files: ${scan.metaFiles.length}`);
|
|
484
514
|
const packs = config.config.packs ?? [{ name: 'default' }];
|
|
485
515
|
console.log(`Packs: ${packs.map(p => p.name).join(', ')}`);
|
package/dist/pipeline.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AssetType, TextureChannelFormat, TextureResize, AudioSampleRate } from
|
|
|
2
2
|
import { parseGlb } from './encoders/glb-loader.js';
|
|
3
3
|
import { dracoPresetToSettings } from './encoders/draco-encoder.js';
|
|
4
4
|
import { buildSkinnedMeshData } from './encoders/skinned-mesh-builder.js';
|
|
5
|
-
import { serializeTextureMetadata, serializeAudioMetadata, serializeMeshMetadata, serializeAnimationClipMetadata, serializeSkinnedMeshMetadata, serializeMaterialSchemaMetadata, deserializeSkinnedMeshMetadata, deserializeAnimationClipMetadata, } from './format/metadata.js';
|
|
5
|
+
import { serializeTextureMetadata, serializeAudioMetadata, serializeMeshMetadata, serializeAnimationClipMetadata, serializeSkinnedMeshMetadata, serializeMaterialSchemaMetadata, serializeSpriteSheetMetadata, deserializeSkinnedMeshMetadata, deserializeAnimationClipMetadata, } from './format/metadata.js';
|
|
6
6
|
import { StowPacker } from './format/packer.js';
|
|
7
7
|
import { BlobStore } from './app/blob-store.js';
|
|
8
8
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
@@ -267,6 +267,22 @@ export function validatePackDependencies(packAssets, allAssetsById) {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
+
// SpriteSheet: check texture reference
|
|
271
|
+
if (asset.type === AssetType.SpriteSheet) {
|
|
272
|
+
const texId = asset.settings.spritesheetConfig.textureAssetId;
|
|
273
|
+
if (texId) {
|
|
274
|
+
const tex = allAssetsById.get(texId);
|
|
275
|
+
if (!tex) {
|
|
276
|
+
errors.push({ assetId: asset.id, message: `SpriteSheet "${asset.stringId}" references missing texture "${texId}"` });
|
|
277
|
+
}
|
|
278
|
+
else if (tex.settings.excluded) {
|
|
279
|
+
errors.push({ assetId: asset.id, message: `SpriteSheet "${asset.stringId}" references excluded texture "${tex.stringId}"` });
|
|
280
|
+
}
|
|
281
|
+
else if (!packIds.has(tex.id)) {
|
|
282
|
+
errors.push({ assetId: asset.id, message: `SpriteSheet "${asset.stringId}" references texture "${tex.stringId}" which is not in the same pack` });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
270
286
|
// AnimationClip: check target mesh reference
|
|
271
287
|
if (asset.type === AssetType.AnimationClip && asset.settings.targetMeshId) {
|
|
272
288
|
const target = allAssetsById.get(asset.settings.targetMeshId);
|
|
@@ -298,6 +314,12 @@ export function buildPack(assets, assetsById) {
|
|
|
298
314
|
packer.addAsset(asset.stringId, AssetType.MaterialSchema, new Uint8Array(0), schemaBytes, asset.settings.tags);
|
|
299
315
|
continue;
|
|
300
316
|
}
|
|
317
|
+
if (asset.type === AssetType.SpriteSheet) {
|
|
318
|
+
const ssMeta = buildSpriteSheetMetadata(asset, assetsById);
|
|
319
|
+
const ssBytes = serializeSpriteSheetMetadata(ssMeta);
|
|
320
|
+
packer.addAsset(asset.stringId, AssetType.SpriteSheet, new Uint8Array(0), ssBytes, asset.settings.tags);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
301
323
|
const processedData = BlobStore.getProcessed(asset.id);
|
|
302
324
|
if (!processedData) {
|
|
303
325
|
console.warn(`[buildPack] Skipping ${asset.id} (type=${asset.type}): no processed data in BlobStore`);
|
|
@@ -433,6 +455,20 @@ function buildMaterialSchemaMetadata(asset, assetsById) {
|
|
|
433
455
|
fields,
|
|
434
456
|
};
|
|
435
457
|
}
|
|
458
|
+
function buildSpriteSheetMetadata(asset, assetsById) {
|
|
459
|
+
const config = asset.settings.spritesheetConfig;
|
|
460
|
+
const textureAsset = config.textureAssetId
|
|
461
|
+
? assetsById.get(config.textureAssetId)
|
|
462
|
+
: undefined;
|
|
463
|
+
return {
|
|
464
|
+
stringId: asset.stringId,
|
|
465
|
+
textureId: textureAsset?.stringId ?? '',
|
|
466
|
+
rows: config.rows,
|
|
467
|
+
columns: config.columns,
|
|
468
|
+
frameCount: config.frameCount,
|
|
469
|
+
frameRate: config.frameRate,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
436
472
|
// ─── Animation data blob builder (v2) ────────────────────────────────────────
|
|
437
473
|
export function buildAnimationDataBlobsV2(imported) {
|
|
438
474
|
const results = [];
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
4
5
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
5
6
|
import { AssetType } from './core/types.js';
|
|
6
7
|
import { defaultAssetSettings } from './app/state.js';
|
|
@@ -8,6 +9,7 @@ import { BlobStore } from './app/blob-store.js';
|
|
|
8
9
|
import { readProjectConfig, scanDirectory, readFile, writeFile, renameFile, deleteFile, getFileSnapshot, probeImageDimensions, } from './node-fs.js';
|
|
9
10
|
import { detectAssetType, readStowmeta, writeStowmeta, stowmetaToAssetSettings, assetSettingsToStowmeta, generateDefaultStowmeta, glbChildToAssetSettings, generateDefaultGlbChild, writeGlbChildSettings, } from './app/stowmeta-io.js';
|
|
10
11
|
import { readStowmat, writeStowmat, stowmatToMaterialConfig, materialConfigToStowmat } from './app/stowmat-io.js';
|
|
12
|
+
import { readStowSpriteSheet, writeStowSpriteSheet, stowSpriteSheetToConfig, spritesheetConfigToStowSpriteSheet } from './app/stowspritesheet-io.js';
|
|
11
13
|
import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, deleteCacheFile, } from './app/process-cache.js';
|
|
12
14
|
import { buildPack, validatePackDependencies, processExtractedAnimations } from './pipeline.js';
|
|
13
15
|
import { syncRuntimeAssets } from './sync-runtime-assets.js';
|
|
@@ -563,6 +565,68 @@ async function readBinaryBody(req) {
|
|
|
563
565
|
chunks.push(chunk);
|
|
564
566
|
return Buffer.concat(chunks);
|
|
565
567
|
}
|
|
568
|
+
// ─── SpriteSheet grid detection ──────────────────────────────────────────────
|
|
569
|
+
const COMMON_TILE_SIZES = [16, 32, 48, 64, 128];
|
|
570
|
+
function detectSpriteGrid(width, height) {
|
|
571
|
+
const result = detectGridDimensions(width, height);
|
|
572
|
+
if (result)
|
|
573
|
+
return { ...result, frameCount: result.columns * result.rows };
|
|
574
|
+
// Absolute fallback: single frame
|
|
575
|
+
return { columns: 1, rows: 1, frameCount: 1 };
|
|
576
|
+
}
|
|
577
|
+
function detectGridDimensions(width, height) {
|
|
578
|
+
// 1. Try common square tile sizes (covers most character/monster/item sheets)
|
|
579
|
+
for (const tile of COMMON_TILE_SIZES) {
|
|
580
|
+
if (width % tile === 0 && height % tile === 0) {
|
|
581
|
+
return { columns: width / tile, rows: height / tile };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// 2. Square frames where frame_size = height (single-row FX sheets)
|
|
585
|
+
if (height < width && width % height === 0) {
|
|
586
|
+
return { columns: width / height, rows: 1 };
|
|
587
|
+
}
|
|
588
|
+
// 3. Near-square single-row: find frame width closest to height among width's divisors
|
|
589
|
+
if (height < 64 && width > height * 1.5) {
|
|
590
|
+
const match = findBestSingleRowCols(width, height);
|
|
591
|
+
if (match)
|
|
592
|
+
return { columns: match, rows: 1 };
|
|
593
|
+
}
|
|
594
|
+
// 4. Non-square common tile combos
|
|
595
|
+
for (const th of COMMON_TILE_SIZES) {
|
|
596
|
+
if (height % th === 0) {
|
|
597
|
+
const rows = height / th;
|
|
598
|
+
for (const tw of COMMON_TILE_SIZES) {
|
|
599
|
+
if (width % tw === 0)
|
|
600
|
+
return { columns: width / tw, rows };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// 5. frame_width = 16 with non-standard height
|
|
605
|
+
if (width % 16 === 0)
|
|
606
|
+
return { columns: width / 16, rows: 1 };
|
|
607
|
+
// 6. Near-square fallback for any size
|
|
608
|
+
const cols = findBestSingleRowCols(width, height);
|
|
609
|
+
if (cols)
|
|
610
|
+
return { columns: cols, rows: 1 };
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function findBestSingleRowCols(width, height) {
|
|
614
|
+
let bestCloseness = Infinity;
|
|
615
|
+
let bestCols = null;
|
|
616
|
+
for (let cols = 1; cols <= width; cols++) {
|
|
617
|
+
if (width % cols !== 0)
|
|
618
|
+
continue;
|
|
619
|
+
const fw = width / cols;
|
|
620
|
+
if (fw < height * 0.4 || fw > height * 2.5)
|
|
621
|
+
continue;
|
|
622
|
+
const closeness = Math.abs(fw / height - 1.0);
|
|
623
|
+
if (closeness < bestCloseness) {
|
|
624
|
+
bestCloseness = closeness;
|
|
625
|
+
bestCols = cols;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return bestCloseness < 0.6 ? bestCols : null;
|
|
629
|
+
}
|
|
566
630
|
function json(res, data, status = 200) {
|
|
567
631
|
res.writeHead(status, {
|
|
568
632
|
'Content-Type': 'application/json',
|
|
@@ -688,6 +752,33 @@ async function openProject(projectDir) {
|
|
|
688
752
|
processedSize: 0,
|
|
689
753
|
});
|
|
690
754
|
}
|
|
755
|
+
// SpriteSheets from .stowspritesheet files
|
|
756
|
+
for (const ssFile of scan.spritesheetFiles) {
|
|
757
|
+
const id = ssFile.relativePath;
|
|
758
|
+
const sheet = await readStowSpriteSheet(projectConfig.srcArtDir, id);
|
|
759
|
+
const fileName = id.split('/').pop() ?? id;
|
|
760
|
+
const baseName = fileName.replace(/\.[^.]+$/, '');
|
|
761
|
+
let meta = await readStowmeta(projectConfig.srcArtDir, id);
|
|
762
|
+
if (!meta) {
|
|
763
|
+
meta = generateDefaultStowmeta(id, AssetType.SpriteSheet, projectConfig.config.defaults);
|
|
764
|
+
await writeStowmeta(projectConfig.srcArtDir, id, meta);
|
|
765
|
+
}
|
|
766
|
+
const spritesheetConfig = sheet ? stowSpriteSheetToConfig(sheet) : { textureAssetId: null, rows: 1, columns: 1, frameCount: 1, frameRate: 12 };
|
|
767
|
+
const settings = defaultAssetSettings();
|
|
768
|
+
settings.spritesheetConfig = spritesheetConfig;
|
|
769
|
+
settings.pack = meta.pack ?? 'default';
|
|
770
|
+
settings.tags = meta.tags ?? [];
|
|
771
|
+
assets.push({
|
|
772
|
+
id,
|
|
773
|
+
fileName: baseName,
|
|
774
|
+
stringId: meta.stringId || baseName,
|
|
775
|
+
type: AssetType.SpriteSheet,
|
|
776
|
+
status: 'ready',
|
|
777
|
+
settings,
|
|
778
|
+
sourceSize: ssFile.size,
|
|
779
|
+
processedSize: 0,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
691
782
|
console.log(`[server] Opened: ${projectConfig.projectName} (${assets.length} assets, ${folders.length} folders)`);
|
|
692
783
|
}
|
|
693
784
|
// ─── Process single asset ─────────────────────────────────────────────────
|
|
@@ -953,6 +1044,7 @@ async function handleRequest(req, res, staticApps) {
|
|
|
953
1044
|
const diskFiles = new Map([
|
|
954
1045
|
...scan.sourceFiles.map(f => [f.relativePath, f]),
|
|
955
1046
|
...scan.matFiles.map(f => [f.relativePath, f]),
|
|
1047
|
+
...scan.spritesheetFiles.map(f => [f.relativePath, f]),
|
|
956
1048
|
]);
|
|
957
1049
|
const diskIds = new Set(diskFiles.keys());
|
|
958
1050
|
// Check for added/removed files
|
|
@@ -964,7 +1056,7 @@ async function handleRequest(req, res, staticApps) {
|
|
|
964
1056
|
const modifiedIds = [];
|
|
965
1057
|
if (!changed) {
|
|
966
1058
|
for (const asset of currentAssets) {
|
|
967
|
-
if (asset.type === AssetType.MaterialSchema)
|
|
1059
|
+
if (asset.type === AssetType.MaterialSchema || asset.type === AssetType.SpriteSheet)
|
|
968
1060
|
continue;
|
|
969
1061
|
const diskFile = diskFiles.get(asset.id);
|
|
970
1062
|
if (diskFile && asset.sourceSize > 0 && diskFile.size !== asset.sourceSize) {
|
|
@@ -1154,6 +1246,10 @@ async function handleRequest(req, res, staticApps) {
|
|
|
1154
1246
|
const stowmat = materialConfigToStowmat(merged.materialConfig);
|
|
1155
1247
|
await writeStowmat(projectConfig.srcArtDir, id, stowmat);
|
|
1156
1248
|
}
|
|
1249
|
+
if (asset.type === AssetType.SpriteSheet && body.settings.spritesheetConfig) {
|
|
1250
|
+
const sheet = spritesheetConfigToStowSpriteSheet(merged.spritesheetConfig);
|
|
1251
|
+
await writeStowSpriteSheet(projectConfig.srcArtDir, id, sheet);
|
|
1252
|
+
}
|
|
1157
1253
|
}
|
|
1158
1254
|
}
|
|
1159
1255
|
// Check if needs reprocessing
|
|
@@ -1423,6 +1519,34 @@ async function handleRequest(req, res, staticApps) {
|
|
|
1423
1519
|
json(res, { ok: true, removedAssets: removedIds.length });
|
|
1424
1520
|
return;
|
|
1425
1521
|
}
|
|
1522
|
+
// POST /api/reveal — open file or folder in OS file explorer
|
|
1523
|
+
if (pathname === '/api/reveal' && req.method === 'POST') {
|
|
1524
|
+
if (!projectConfig) {
|
|
1525
|
+
json(res, { error: 'No project open' }, 400);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const body = JSON.parse(await readBody(req));
|
|
1529
|
+
const relativePath = body.path;
|
|
1530
|
+
if (!relativePath) {
|
|
1531
|
+
json(res, { error: 'Missing path' }, 400);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const fullPath = path.resolve(projectConfig.srcArtDir, relativePath);
|
|
1535
|
+
// Ensure the path is inside the project
|
|
1536
|
+
if (!fullPath.startsWith(path.resolve(projectConfig.srcArtDir))) {
|
|
1537
|
+
json(res, { error: 'Path outside project' }, 403);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
1541
|
+
if (stat?.isDirectory()) {
|
|
1542
|
+
exec(`explorer "${fullPath}"`);
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
exec(`explorer /select,"${fullPath}"`);
|
|
1546
|
+
}
|
|
1547
|
+
json(res, { ok: true });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1426
1550
|
// GET /api/asset/:id/source — serve source file for preview
|
|
1427
1551
|
if (pathname.startsWith('/api/asset/') && pathname.endsWith('/source') && req.method === 'GET') {
|
|
1428
1552
|
const id = decodeURIComponent(pathname.slice('/api/asset/'.length, -'/source'.length));
|
|
@@ -1559,6 +1683,210 @@ async function handleRequest(req, res, staticApps) {
|
|
|
1559
1683
|
json(res, { ok: true, asset });
|
|
1560
1684
|
return;
|
|
1561
1685
|
}
|
|
1686
|
+
// POST /api/create-spritesheet
|
|
1687
|
+
if (pathname === '/api/create-spritesheet' && req.method === 'POST') {
|
|
1688
|
+
if (!projectConfig) {
|
|
1689
|
+
json(res, { error: 'No project open' }, 400);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const body = JSON.parse(await readBody(req));
|
|
1693
|
+
const targetFolder = body.targetFolder ?? '';
|
|
1694
|
+
const name = body.name?.trim() || `SpriteSheet ${assets.filter(a => a.type === AssetType.SpriteSheet).length + 1}`;
|
|
1695
|
+
const baseName = `${name.replace(/\s+/g, '_')}.stowspritesheet`;
|
|
1696
|
+
const fileName = targetFolder ? `${targetFolder}/${baseName}` : baseName;
|
|
1697
|
+
const settings = defaultAssetSettings();
|
|
1698
|
+
settings.spritesheetConfig = {
|
|
1699
|
+
textureAssetId: null,
|
|
1700
|
+
rows: 1,
|
|
1701
|
+
columns: 1,
|
|
1702
|
+
frameCount: 1,
|
|
1703
|
+
frameRate: 12,
|
|
1704
|
+
};
|
|
1705
|
+
const asset = {
|
|
1706
|
+
id: fileName,
|
|
1707
|
+
fileName: name,
|
|
1708
|
+
stringId: name,
|
|
1709
|
+
type: AssetType.SpriteSheet,
|
|
1710
|
+
status: 'ready',
|
|
1711
|
+
settings,
|
|
1712
|
+
sourceSize: 0,
|
|
1713
|
+
processedSize: 0,
|
|
1714
|
+
};
|
|
1715
|
+
assets.push(asset);
|
|
1716
|
+
const stowSpriteSheet = spritesheetConfigToStowSpriteSheet(settings.spritesheetConfig);
|
|
1717
|
+
await writeStowSpriteSheet(projectConfig.srcArtDir, fileName, stowSpriteSheet);
|
|
1718
|
+
const meta = assetSettingsToStowmeta(asset);
|
|
1719
|
+
await writeStowmeta(projectConfig.srcArtDir, fileName, meta);
|
|
1720
|
+
json(res, { ok: true, asset });
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
// POST /api/convert-to-spritesheet — auto-detect grid from a texture and create a .stowspritesheet
|
|
1724
|
+
if (pathname === '/api/convert-to-spritesheet' && req.method === 'POST') {
|
|
1725
|
+
if (!projectConfig) {
|
|
1726
|
+
json(res, { error: 'No project open' }, 400);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const body = JSON.parse(await readBody(req));
|
|
1730
|
+
const textureId = body.textureId;
|
|
1731
|
+
if (!textureId) {
|
|
1732
|
+
json(res, { error: 'Missing textureId' }, 400);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const textureAsset = assets.find(a => a.id === textureId);
|
|
1736
|
+
if (!textureAsset || textureAsset.type !== AssetType.Texture2D) {
|
|
1737
|
+
json(res, { error: 'Asset is not a texture' }, 400);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
// Use caller-supplied grid override, otherwise heuristic-detect from image dimensions
|
|
1741
|
+
let rows;
|
|
1742
|
+
let columns;
|
|
1743
|
+
if (typeof body.rows === 'number' && body.rows > 0 && typeof body.columns === 'number' && body.columns > 0) {
|
|
1744
|
+
rows = body.rows;
|
|
1745
|
+
columns = body.columns;
|
|
1746
|
+
}
|
|
1747
|
+
else {
|
|
1748
|
+
const dims = await probeImageDimensions(projectConfig.srcArtDir, textureId);
|
|
1749
|
+
if (!dims) {
|
|
1750
|
+
json(res, { error: 'Could not read image dimensions' }, 500);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
({ rows, columns } = detectSpriteGrid(dims.width, dims.height));
|
|
1754
|
+
}
|
|
1755
|
+
const frameCount = rows * columns;
|
|
1756
|
+
// Build name: foldername_imagename_animation
|
|
1757
|
+
const parts = textureId.replace(/\\/g, '/').split('/');
|
|
1758
|
+
const stem = parts[parts.length - 1].replace(/\.[^.]+$/, '').replace(/^SpriteSheet/, '').replace(/^spritesheet/i, '');
|
|
1759
|
+
const folder = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
1760
|
+
const safeName = [folder, stem].filter(Boolean).join('_').replace(/\s+/g, '_').toLowerCase();
|
|
1761
|
+
const sheetName = `${safeName}_animation`;
|
|
1762
|
+
const targetFolder = parts.slice(0, -1).join('/');
|
|
1763
|
+
const baseName = `${sheetName}.stowspritesheet`;
|
|
1764
|
+
const fileName = targetFolder ? `${targetFolder}/${baseName}` : baseName;
|
|
1765
|
+
// Check if already exists
|
|
1766
|
+
if (assets.find(a => a.id === fileName)) {
|
|
1767
|
+
json(res, { error: `Spritesheet already exists: ${fileName}` }, 409);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const settings = defaultAssetSettings();
|
|
1771
|
+
settings.spritesheetConfig = {
|
|
1772
|
+
textureAssetId: textureId,
|
|
1773
|
+
rows,
|
|
1774
|
+
columns,
|
|
1775
|
+
frameCount,
|
|
1776
|
+
frameRate: 12,
|
|
1777
|
+
};
|
|
1778
|
+
const asset = {
|
|
1779
|
+
id: fileName,
|
|
1780
|
+
fileName: sheetName,
|
|
1781
|
+
stringId: fileName.replace(/\.[^.]+$/, '').toLowerCase(),
|
|
1782
|
+
type: AssetType.SpriteSheet,
|
|
1783
|
+
status: 'ready',
|
|
1784
|
+
settings,
|
|
1785
|
+
sourceSize: 0,
|
|
1786
|
+
processedSize: 0,
|
|
1787
|
+
};
|
|
1788
|
+
assets.push(asset);
|
|
1789
|
+
const stowSpriteSheet = spritesheetConfigToStowSpriteSheet(settings.spritesheetConfig);
|
|
1790
|
+
await writeStowSpriteSheet(projectConfig.srcArtDir, fileName, stowSpriteSheet);
|
|
1791
|
+
const meta = assetSettingsToStowmeta(asset);
|
|
1792
|
+
await writeStowmeta(projectConfig.srcArtDir, fileName, meta);
|
|
1793
|
+
broadcast({ type: 'refresh' });
|
|
1794
|
+
json(res, { ok: true, asset });
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
// POST /api/batch-convert-folder-name — find all folders matching a name and convert all textures under them to sprite sheets
|
|
1798
|
+
if (pathname === '/api/batch-convert-folder-name' && req.method === 'POST') {
|
|
1799
|
+
if (!projectConfig) {
|
|
1800
|
+
json(res, { error: 'No project open' }, 400);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const body = JSON.parse(await readBody(req));
|
|
1804
|
+
const folderName = body.folderName;
|
|
1805
|
+
if (!folderName) {
|
|
1806
|
+
json(res, { error: 'Missing folderName' }, 400);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
// Find all folders that match the given name (case-insensitive)
|
|
1810
|
+
const matchingFolders = folders.filter(f => {
|
|
1811
|
+
const parts = f.replace(/\\/g, '/').split('/');
|
|
1812
|
+
return parts[parts.length - 1].toLowerCase() === folderName.toLowerCase();
|
|
1813
|
+
});
|
|
1814
|
+
if (matchingFolders.length === 0) {
|
|
1815
|
+
json(res, { error: `No folders found matching "${folderName}"` }, 404);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
// Find all Texture2D assets under any of the matching folders
|
|
1819
|
+
const textureIds = [];
|
|
1820
|
+
for (const folder of matchingFolders) {
|
|
1821
|
+
const prefix = folder + '/';
|
|
1822
|
+
for (const asset of assets) {
|
|
1823
|
+
if (asset.type === AssetType.Texture2D && (asset.id.startsWith(prefix) || asset.id.replace(/\\/g, '/').startsWith(prefix))) {
|
|
1824
|
+
textureIds.push(asset.id);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (textureIds.length === 0) {
|
|
1829
|
+
json(res, { error: `No textures found under folders matching "${folderName}"` }, 404);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
// Convert each texture to a sprite sheet (reuse the same logic as convert-to-spritesheet)
|
|
1833
|
+
let succeeded = 0;
|
|
1834
|
+
let failed = 0;
|
|
1835
|
+
const created = [];
|
|
1836
|
+
for (const textureId of textureIds) {
|
|
1837
|
+
const textureAsset = assets.find(a => a.id === textureId);
|
|
1838
|
+
if (!textureAsset) {
|
|
1839
|
+
failed++;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
const dims = await probeImageDimensions(projectConfig.srcArtDir, textureId);
|
|
1843
|
+
if (!dims) {
|
|
1844
|
+
failed++;
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const { columns, rows, frameCount } = detectSpriteGrid(dims.width, dims.height);
|
|
1848
|
+
const parts = textureId.replace(/\\/g, '/').split('/');
|
|
1849
|
+
const stem = parts[parts.length - 1].replace(/\.[^.]+$/, '').replace(/^SpriteSheet/, '').replace(/^spritesheet/i, '');
|
|
1850
|
+
const folder = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
1851
|
+
const safeName = [folder, stem].filter(Boolean).join('_').replace(/\s+/g, '_').toLowerCase();
|
|
1852
|
+
const sheetName = `${safeName}_animation`;
|
|
1853
|
+
const targetFolder = parts.slice(0, -1).join('/');
|
|
1854
|
+
const baseName = `${sheetName}.stowspritesheet`;
|
|
1855
|
+
const fileName = targetFolder ? `${targetFolder}/${baseName}` : baseName;
|
|
1856
|
+
if (assets.find(a => a.id === fileName)) {
|
|
1857
|
+
failed++;
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const settings = defaultAssetSettings();
|
|
1861
|
+
settings.spritesheetConfig = {
|
|
1862
|
+
textureAssetId: textureId,
|
|
1863
|
+
rows,
|
|
1864
|
+
columns,
|
|
1865
|
+
frameCount,
|
|
1866
|
+
frameRate: 12,
|
|
1867
|
+
};
|
|
1868
|
+
const asset = {
|
|
1869
|
+
id: fileName,
|
|
1870
|
+
fileName: sheetName,
|
|
1871
|
+
stringId: fileName.replace(/\.[^.]+$/, '').toLowerCase(),
|
|
1872
|
+
type: AssetType.SpriteSheet,
|
|
1873
|
+
status: 'ready',
|
|
1874
|
+
settings,
|
|
1875
|
+
sourceSize: 0,
|
|
1876
|
+
processedSize: 0,
|
|
1877
|
+
};
|
|
1878
|
+
assets.push(asset);
|
|
1879
|
+
const stowSpriteSheet = spritesheetConfigToStowSpriteSheet(settings.spritesheetConfig);
|
|
1880
|
+
await writeStowSpriteSheet(projectConfig.srcArtDir, fileName, stowSpriteSheet);
|
|
1881
|
+
const meta = assetSettingsToStowmeta(asset);
|
|
1882
|
+
await writeStowmeta(projectConfig.srcArtDir, fileName, meta);
|
|
1883
|
+
created.push(asset);
|
|
1884
|
+
succeeded++;
|
|
1885
|
+
}
|
|
1886
|
+
broadcast({ type: 'refresh' });
|
|
1887
|
+
json(res, { ok: true, succeeded, failed, matchingFolders, assets: created });
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1562
1890
|
// POST /api/create-folder
|
|
1563
1891
|
if (pathname === '/api/create-folder' && req.method === 'POST') {
|
|
1564
1892
|
if (!projectConfig) {
|
|
@@ -1973,6 +2301,31 @@ async function handleRequest(req, res, staticApps) {
|
|
|
1973
2301
|
}
|
|
1974
2302
|
return;
|
|
1975
2303
|
}
|
|
2304
|
+
// POST /api/asset-store/package/:name/delist — remove package from registry (keeps GCS files)
|
|
2305
|
+
if (pathname.startsWith('/api/asset-store/package/') && pathname.endsWith('/delist') && req.method === 'POST') {
|
|
2306
|
+
if (!projectConfig) {
|
|
2307
|
+
json(res, { error: 'No project open' }, 400);
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
const packageName = decodeURIComponent(pathname.slice('/api/asset-store/package/'.length, -'/delist'.length));
|
|
2312
|
+
const { createFirestoreClient } = await import('./firestore.js');
|
|
2313
|
+
const firestore = await createFirestoreClient(projectConfig.projectDir);
|
|
2314
|
+
const pkg = await firestore.getPackage(packageName);
|
|
2315
|
+
if (!pkg) {
|
|
2316
|
+
json(res, { error: `Package "${packageName}" not found` }, 404);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
await firestore.deletePackage(packageName);
|
|
2320
|
+
const { clearFirestoreCache } = await import('./firestore.js');
|
|
2321
|
+
clearFirestoreCache();
|
|
2322
|
+
json(res, { ok: true, packageName });
|
|
2323
|
+
}
|
|
2324
|
+
catch (err) {
|
|
2325
|
+
json(res, { error: err.message }, 500);
|
|
2326
|
+
}
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
1976
2329
|
// POST /api/publish — publish assets to GCS (SSE stream with progress)
|
|
1977
2330
|
if (pathname === '/api/publish' && req.method === 'POST') {
|
|
1978
2331
|
if (!projectConfig) {
|
|
@@ -2300,6 +2653,159 @@ async function handleRequest(req, res, staticApps) {
|
|
|
2300
2653
|
}
|
|
2301
2654
|
return;
|
|
2302
2655
|
}
|
|
2656
|
+
// POST /api/ai/guess-spritesheet-grid/:id — ask Gemini to count rows/columns on a texture without creating anything
|
|
2657
|
+
if (pathname.startsWith('/api/ai/guess-spritesheet-grid/') && req.method === 'POST') {
|
|
2658
|
+
if (!projectConfig) {
|
|
2659
|
+
json(res, { error: 'No project open' }, 400);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const apiKey = projectConfig.config.ai?.geminiApiKey;
|
|
2663
|
+
if (!apiKey) {
|
|
2664
|
+
json(res, { error: 'Gemini API key not configured' }, 400);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const id = decodeURIComponent(pathname.slice('/api/ai/guess-spritesheet-grid/'.length));
|
|
2668
|
+
const asset = assets.find(a => a.id === id);
|
|
2669
|
+
if (!asset || asset.type !== AssetType.Texture2D) {
|
|
2670
|
+
json(res, { error: 'Asset is not a texture' }, 404);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
try {
|
|
2674
|
+
const raw = await readFile(projectConfig.srcArtDir, id);
|
|
2675
|
+
if (!raw) {
|
|
2676
|
+
json(res, { error: 'Source file not found' }, 404);
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const ext = id.split('.').pop()?.toLowerCase() ?? 'png';
|
|
2680
|
+
const mimeMap = {
|
|
2681
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif',
|
|
2682
|
+
};
|
|
2683
|
+
const mimeType = mimeMap[ext] ?? 'image/png';
|
|
2684
|
+
const { detectSpriteSheet: aiDetectSpriteSheet } = await import('./ai-tagger.js');
|
|
2685
|
+
const detection = await aiDetectSpriteSheet(apiKey, Buffer.from(raw), mimeType);
|
|
2686
|
+
if (detection.isSpriteSheet && detection.rows && detection.columns) {
|
|
2687
|
+
json(res, { rows: detection.rows, columns: detection.columns });
|
|
2688
|
+
}
|
|
2689
|
+
else {
|
|
2690
|
+
// Fall back to heuristic
|
|
2691
|
+
const dims = await probeImageDimensions(projectConfig.srcArtDir, id);
|
|
2692
|
+
const grid = dims ? detectSpriteGrid(dims.width, dims.height) : { rows: 1, columns: 1 };
|
|
2693
|
+
json(res, { rows: grid.rows, columns: grid.columns });
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
catch (err) {
|
|
2697
|
+
json(res, { error: err.message }, 500);
|
|
2698
|
+
}
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
// POST /api/ai/detect-spritesheet-batch — use Gemini to detect sprite sheets across multiple textures concurrently
|
|
2702
|
+
if (pathname === '/api/ai/detect-spritesheet-batch' && req.method === 'POST') {
|
|
2703
|
+
if (!projectConfig) {
|
|
2704
|
+
json(res, { error: 'No project open' }, 400);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
const apiKey = projectConfig.config.ai?.geminiApiKey;
|
|
2708
|
+
if (!apiKey) {
|
|
2709
|
+
json(res, { error: 'Gemini API key not configured' }, 400);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
const body = JSON.parse(await readBody(req));
|
|
2714
|
+
const ids = body.ids;
|
|
2715
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
2716
|
+
json(res, { error: 'ids must be a non-empty array' }, 400);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
const { detectSpriteSheet: aiDetectSpriteSheet } = await import('./ai-tagger.js');
|
|
2720
|
+
const mimeMap = {
|
|
2721
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif',
|
|
2722
|
+
};
|
|
2723
|
+
let created = 0;
|
|
2724
|
+
let notSpriteSheet = 0;
|
|
2725
|
+
let alreadyExists = 0;
|
|
2726
|
+
const errors = [];
|
|
2727
|
+
const results = await Promise.allSettled(ids.map(async (id) => {
|
|
2728
|
+
const asset = assets.find(a => a.id === id);
|
|
2729
|
+
if (!asset || asset.type !== AssetType.Texture2D)
|
|
2730
|
+
throw new Error('Not a texture');
|
|
2731
|
+
const raw = await readFile(projectConfig.srcArtDir, id);
|
|
2732
|
+
if (!raw)
|
|
2733
|
+
throw new Error(`Source file not found: ${id}`);
|
|
2734
|
+
const ext = id.split('.').pop()?.toLowerCase() ?? 'png';
|
|
2735
|
+
const mimeType = mimeMap[ext] ?? 'image/png';
|
|
2736
|
+
const detection = await aiDetectSpriteSheet(apiKey, Buffer.from(raw), mimeType);
|
|
2737
|
+
if (!detection.isSpriteSheet)
|
|
2738
|
+
return 'notSpriteSheet';
|
|
2739
|
+
// Use AI-detected grid, falling back to heuristic
|
|
2740
|
+
let rows;
|
|
2741
|
+
let columns;
|
|
2742
|
+
if (detection.rows && detection.columns) {
|
|
2743
|
+
rows = detection.rows;
|
|
2744
|
+
columns = detection.columns;
|
|
2745
|
+
}
|
|
2746
|
+
else {
|
|
2747
|
+
const dims = await probeImageDimensions(projectConfig.srcArtDir, id);
|
|
2748
|
+
if (!dims)
|
|
2749
|
+
throw new Error('Could not read image dimensions');
|
|
2750
|
+
const grid = detectSpriteGrid(dims.width, dims.height);
|
|
2751
|
+
rows = grid.rows;
|
|
2752
|
+
columns = grid.columns;
|
|
2753
|
+
}
|
|
2754
|
+
const frameCount = rows * columns;
|
|
2755
|
+
const parts = id.replace(/\\/g, '/').split('/');
|
|
2756
|
+
const stem = parts[parts.length - 1].replace(/\.[^.]+$/, '').replace(/^SpriteSheet/, '').replace(/^spritesheet/i, '');
|
|
2757
|
+
const folder = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
2758
|
+
const safeName = [folder, stem].filter(Boolean).join('_').replace(/\s+/g, '_').toLowerCase();
|
|
2759
|
+
const sheetName = `${safeName}_animation`;
|
|
2760
|
+
const targetFolder = parts.slice(0, -1).join('/');
|
|
2761
|
+
const baseName = `${sheetName}.stowspritesheet`;
|
|
2762
|
+
const fileName = targetFolder ? `${targetFolder}/${baseName}` : baseName;
|
|
2763
|
+
if (assets.find(a => a.id === fileName))
|
|
2764
|
+
return 'alreadyExists';
|
|
2765
|
+
const settings = defaultAssetSettings();
|
|
2766
|
+
settings.spritesheetConfig = { textureAssetId: id, rows, columns, frameCount, frameRate: 12 };
|
|
2767
|
+
const newAsset = {
|
|
2768
|
+
id: fileName,
|
|
2769
|
+
fileName: sheetName,
|
|
2770
|
+
stringId: fileName.replace(/\.[^.]+$/, '').toLowerCase(),
|
|
2771
|
+
type: AssetType.SpriteSheet,
|
|
2772
|
+
status: 'ready',
|
|
2773
|
+
settings,
|
|
2774
|
+
sourceSize: 0,
|
|
2775
|
+
processedSize: 0,
|
|
2776
|
+
};
|
|
2777
|
+
assets.push(newAsset);
|
|
2778
|
+
const stowSpriteSheet = spritesheetConfigToStowSpriteSheet(settings.spritesheetConfig);
|
|
2779
|
+
await writeStowSpriteSheet(projectConfig.srcArtDir, fileName, stowSpriteSheet);
|
|
2780
|
+
const newMeta = assetSettingsToStowmeta(newAsset);
|
|
2781
|
+
await writeStowmeta(projectConfig.srcArtDir, fileName, newMeta);
|
|
2782
|
+
return 'created';
|
|
2783
|
+
}));
|
|
2784
|
+
for (let i = 0; i < results.length; i++) {
|
|
2785
|
+
const r = results[i];
|
|
2786
|
+
if (r.status === 'fulfilled') {
|
|
2787
|
+
if (r.value === 'created')
|
|
2788
|
+
created++;
|
|
2789
|
+
else if (r.value === 'notSpriteSheet')
|
|
2790
|
+
notSpriteSheet++;
|
|
2791
|
+
else if (r.value === 'alreadyExists')
|
|
2792
|
+
alreadyExists++;
|
|
2793
|
+
}
|
|
2794
|
+
else {
|
|
2795
|
+
const errMsg = r.reason?.message ?? String(r.reason);
|
|
2796
|
+
console.error(`[ai-detect-spritesheet] ${ids[i]}: ${errMsg}`);
|
|
2797
|
+
errors.push({ id: ids[i], error: errMsg });
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (created > 0)
|
|
2801
|
+
broadcast({ type: 'refresh' });
|
|
2802
|
+
json(res, { created, notSpriteSheet, alreadyExists, errors });
|
|
2803
|
+
}
|
|
2804
|
+
catch (err) {
|
|
2805
|
+
json(res, { error: err.message }, 500);
|
|
2806
|
+
}
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2303
2809
|
// GET /api/thumbnails/cached — return which thumbnails are still valid in the cache
|
|
2304
2810
|
if (pathname === '/api/thumbnails/cached' && req.method === 'GET') {
|
|
2305
2811
|
if (!projectConfig) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/stowkit-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.41",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@google/genai": "^1.46.0",
|
|
22
22
|
"@series-inc/stowkit-editor": "^0.1.8",
|
|
23
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
23
|
+
"@series-inc/stowkit-packer-gui": "^0.1.30",
|
|
24
24
|
"@strangeape/ffmpeg-audio-wasm": "^0.1.0",
|
|
25
25
|
"draco3d": "^1.5.7",
|
|
26
26
|
"fbx-parser": "^2.1.3",
|