@series-inc/stowkit-cli 0.6.17 → 0.6.19
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/disk-project.d.ts +24 -1
- package/dist/app/disk-project.js +7 -0
- package/dist/app/process-cache.js +1 -1
- package/dist/app/state.d.ts +2 -1
- package/dist/app/state.js +2 -1
- package/dist/app/stowmeta-io.d.ts +6 -3
- package/dist/app/stowmeta-io.js +62 -21
- package/dist/app/thumbnail-cache.d.ts +29 -0
- package/dist/app/thumbnail-cache.js +137 -0
- package/dist/assets-package.d.ts +66 -0
- package/dist/assets-package.js +80 -0
- package/dist/cleanup.js +11 -2
- package/dist/cli.js +47 -0
- package/dist/core/constants.d.ts +4 -2
- package/dist/core/constants.js +4 -2
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.js +5 -0
- package/dist/encoders/basis-encoder.js +2 -1
- package/dist/format/metadata.js +12 -7
- package/dist/gcs.d.ts +10 -0
- package/dist/gcs.js +158 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/node-fs.d.ts +4 -0
- package/dist/node-fs.js +14 -0
- package/dist/orchestrator.js +37 -10
- package/dist/pipeline.js +1 -0
- package/dist/publish.d.ts +27 -0
- package/dist/publish.js +418 -0
- package/dist/server.js +567 -20
- package/dist/store.d.ts +50 -0
- package/dist/store.js +305 -0
- package/package.json +3 -3
- package/skill.md +63 -0
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import type { KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
1
|
+
import type { KTX2Quality, TextureResize, TextureFilterMode, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
2
2
|
export interface PackConfig {
|
|
3
3
|
name: string;
|
|
4
4
|
}
|
|
5
|
+
export interface TextureDefaults {
|
|
6
|
+
quality?: string;
|
|
7
|
+
resize?: string;
|
|
8
|
+
filtering?: string;
|
|
9
|
+
generateMipmaps?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioDefaults {
|
|
12
|
+
aacQuality?: string;
|
|
13
|
+
sampleRate?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MeshDefaults {
|
|
16
|
+
dracoQuality?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ProjectDefaults {
|
|
19
|
+
texture?: TextureDefaults;
|
|
20
|
+
audio?: AudioDefaults;
|
|
21
|
+
mesh?: MeshDefaults;
|
|
22
|
+
}
|
|
5
23
|
export interface FelicityProject {
|
|
6
24
|
srcArtDir: string;
|
|
7
25
|
name?: string;
|
|
8
26
|
cdnAssetsPath?: string;
|
|
9
27
|
prefabsPath?: string;
|
|
10
28
|
packs?: PackConfig[];
|
|
29
|
+
defaults?: ProjectDefaults;
|
|
11
30
|
}
|
|
12
31
|
export interface FileSnapshot {
|
|
13
32
|
relativePath: string;
|
|
@@ -33,6 +52,7 @@ export interface StowMetaTexture extends StowMetaBase {
|
|
|
33
52
|
type: 'texture';
|
|
34
53
|
quality: string;
|
|
35
54
|
resize: string;
|
|
55
|
+
filtering?: string;
|
|
36
56
|
generateMipmaps: boolean;
|
|
37
57
|
}
|
|
38
58
|
export interface StowMetaAudio extends StowMetaBase {
|
|
@@ -67,6 +87,7 @@ export interface StowMetaGlbChild {
|
|
|
67
87
|
cache?: StowMetaCache;
|
|
68
88
|
quality?: string;
|
|
69
89
|
resize?: string;
|
|
90
|
+
filtering?: string;
|
|
70
91
|
generateMipmaps?: boolean;
|
|
71
92
|
aacQuality?: string;
|
|
72
93
|
sampleRate?: string;
|
|
@@ -101,6 +122,8 @@ export declare const KTX2_QUALITY_STRINGS: Record<string, KTX2Quality>;
|
|
|
101
122
|
export declare const KTX2_QUALITY_TO_STRING: Record<number, string>;
|
|
102
123
|
export declare const TEXTURE_RESIZE_STRINGS: Record<string, TextureResize>;
|
|
103
124
|
export declare const TEXTURE_RESIZE_TO_STRING: Record<number, string>;
|
|
125
|
+
export declare const TEXTURE_FILTER_STRINGS: Record<string, TextureFilterMode>;
|
|
126
|
+
export declare const TEXTURE_FILTER_TO_STRING: Record<number, string>;
|
|
104
127
|
export declare const DRACO_QUALITY_STRINGS: Record<string, DracoQualityPreset>;
|
|
105
128
|
export declare const DRACO_QUALITY_TO_STRING: Record<number, string>;
|
|
106
129
|
export declare const AAC_QUALITY_STRINGS: Record<string, AacQuality>;
|
package/dist/app/disk-project.js
CHANGED
|
@@ -18,6 +18,13 @@ export const TEXTURE_RESIZE_STRINGS = {
|
|
|
18
18
|
export const TEXTURE_RESIZE_TO_STRING = {
|
|
19
19
|
0: 'full', 1: 'half', 2: 'quarter', 3: 'eighth',
|
|
20
20
|
};
|
|
21
|
+
export const TEXTURE_FILTER_STRINGS = {
|
|
22
|
+
linear: 0,
|
|
23
|
+
nearest: 1,
|
|
24
|
+
};
|
|
25
|
+
export const TEXTURE_FILTER_TO_STRING = {
|
|
26
|
+
0: 'linear', 1: 'nearest',
|
|
27
|
+
};
|
|
21
28
|
export const DRACO_QUALITY_STRINGS = {
|
|
22
29
|
fast: 0,
|
|
23
30
|
balanced: 1,
|
|
@@ -96,7 +96,7 @@ export function computeSettingsHash(type, settings) {
|
|
|
96
96
|
const parts = [String(type)];
|
|
97
97
|
switch (type) {
|
|
98
98
|
case 2: // Texture2D
|
|
99
|
-
parts.push(`q=${settings.quality}`, `r=${settings.resize}`, `m=${settings.generateMipmaps}`);
|
|
99
|
+
parts.push(`q=${settings.quality}`, `r=${settings.resize}`, `f=${settings.filtering}`, `m=${settings.generateMipmaps}`);
|
|
100
100
|
break;
|
|
101
101
|
case 3: // Audio
|
|
102
102
|
parts.push(`aq=${settings.aacQuality}`, `sr=${settings.audioSampleRate}`);
|
package/dist/app/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssetType, TextureMetadata, AudioMetadata, MeshMetadata, AnimationClipMetadata, SkinnedMeshMetadata, KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
1
|
+
import type { AssetType, TextureMetadata, AudioMetadata, MeshMetadata, AnimationClipMetadata, SkinnedMeshMetadata, 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;
|
|
@@ -13,6 +13,7 @@ export interface MaterialConfig {
|
|
|
13
13
|
export interface AssetSettings {
|
|
14
14
|
quality: KTX2Quality;
|
|
15
15
|
resize: TextureResize;
|
|
16
|
+
filtering: TextureFilterMode;
|
|
16
17
|
generateMipmaps: boolean;
|
|
17
18
|
tags: string[];
|
|
18
19
|
dracoQuality: DracoQualityPreset;
|
package/dist/app/state.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { KTX2Quality as KQ, TextureResize as TR, DracoQualityPreset as DQ, AacQuality as AQ, AudioSampleRate as ASR, } from '../core/types.js';
|
|
1
|
+
import { KTX2Quality as KQ, TextureResize as TR, TextureFilterMode as TFM, DracoQualityPreset as DQ, AacQuality as AQ, AudioSampleRate as ASR, } from '../core/types.js';
|
|
2
2
|
export function defaultAssetSettings() {
|
|
3
3
|
return {
|
|
4
4
|
quality: KQ.Fastest,
|
|
5
5
|
resize: TR.Full,
|
|
6
|
+
filtering: TFM.Linear,
|
|
6
7
|
generateMipmaps: false,
|
|
7
8
|
tags: [],
|
|
8
9
|
dracoQuality: DQ.Balanced,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssetSettings } from './state.js';
|
|
2
2
|
import { AssetType } from '../core/types.js';
|
|
3
3
|
import type { ProjectAsset } from './state.js';
|
|
4
|
-
import type { StowMeta, StowMetaGlbChild } from './disk-project.js';
|
|
4
|
+
import type { ProjectDefaults, StowMeta, StowMetaGlbChild } from './disk-project.js';
|
|
5
5
|
export declare function detectAssetType(fileName: string): AssetType;
|
|
6
6
|
export declare function stowmetaPath(relativePath: string): string;
|
|
7
7
|
export declare function readStowmeta(srcArtDir: string, relativePath: string): Promise<StowMeta | null>;
|
|
@@ -11,9 +11,12 @@ export declare function stowmetaToAssetSettings(meta: StowMeta): {
|
|
|
11
11
|
settings: AssetSettings;
|
|
12
12
|
};
|
|
13
13
|
export declare function assetSettingsToStowmeta(asset: ProjectAsset): StowMeta;
|
|
14
|
-
export declare function generateDefaultStowmeta(relativePath: string, type?: AssetType
|
|
14
|
+
export declare function generateDefaultStowmeta(relativePath: string, type?: AssetType, projectDefaults?: ProjectDefaults, imageDimensions?: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
} | null): StowMeta;
|
|
15
18
|
/** Generate a default inline child entry for a GLB container. */
|
|
16
|
-
export declare function generateDefaultGlbChild(name: string, childType: string): StowMetaGlbChild;
|
|
19
|
+
export declare function generateDefaultGlbChild(name: string, childType: string, projectDefaults?: ProjectDefaults): StowMetaGlbChild;
|
|
17
20
|
/** Convert an inline child entry to { type, settings } for use in ProjectAsset. */
|
|
18
21
|
export declare function glbChildToAssetSettings(child: StowMetaGlbChild): {
|
|
19
22
|
type: AssetType;
|
package/dist/app/stowmeta-io.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AssetType } from '../core/types.js';
|
|
2
|
-
import { KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
3
|
-
import { KTX2_QUALITY_STRINGS, KTX2_QUALITY_TO_STRING, TEXTURE_RESIZE_STRINGS, TEXTURE_RESIZE_TO_STRING, DRACO_QUALITY_STRINGS, DRACO_QUALITY_TO_STRING, AAC_QUALITY_STRINGS, AAC_QUALITY_TO_STRING, AUDIO_SAMPLE_RATE_STRINGS, AUDIO_SAMPLE_RATE_TO_STRING, MATERIAL_FIELD_TYPE_STRINGS, MATERIAL_FIELD_TYPE_TO_STRING, PREVIEW_FLAG_STRINGS, PREVIEW_FLAG_TO_STRING, } from './disk-project.js';
|
|
2
|
+
import { KTX2Quality, TextureResize, TextureFilterMode, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
3
|
+
import { KTX2_QUALITY_STRINGS, KTX2_QUALITY_TO_STRING, TEXTURE_RESIZE_STRINGS, TEXTURE_RESIZE_TO_STRING, TEXTURE_FILTER_STRINGS, TEXTURE_FILTER_TO_STRING, DRACO_QUALITY_STRINGS, DRACO_QUALITY_TO_STRING, AAC_QUALITY_STRINGS, AAC_QUALITY_TO_STRING, AUDIO_SAMPLE_RATE_STRINGS, AUDIO_SAMPLE_RATE_TO_STRING, MATERIAL_FIELD_TYPE_STRINGS, MATERIAL_FIELD_TYPE_TO_STRING, PREVIEW_FLAG_STRINGS, PREVIEW_FLAG_TO_STRING, } from './disk-project.js';
|
|
4
4
|
import { readTextFile, writeFile } from '../node-fs.js';
|
|
5
5
|
import { defaultAssetSettings } from './state.js';
|
|
6
6
|
// ─── Detect asset type from file extension ──────────────────────────────────
|
|
@@ -70,6 +70,7 @@ export function stowmetaToAssetSettings(meta) {
|
|
|
70
70
|
...base,
|
|
71
71
|
quality: KTX2_QUALITY_STRINGS[meta.quality] ?? KTX2Quality.Fastest,
|
|
72
72
|
resize: TEXTURE_RESIZE_STRINGS[meta.resize] ?? TextureResize.Full,
|
|
73
|
+
filtering: TEXTURE_FILTER_STRINGS[meta.filtering ?? 'linear'] ?? TextureFilterMode.Linear,
|
|
73
74
|
generateMipmaps: meta.generateMipmaps ?? false,
|
|
74
75
|
},
|
|
75
76
|
};
|
|
@@ -152,6 +153,7 @@ export function assetSettingsToStowmeta(asset) {
|
|
|
152
153
|
type: 'texture',
|
|
153
154
|
quality: KTX2_QUALITY_TO_STRING[asset.settings.quality] ?? 'fastest',
|
|
154
155
|
resize: TEXTURE_RESIZE_TO_STRING[asset.settings.resize] ?? 'full',
|
|
156
|
+
filtering: TEXTURE_FILTER_TO_STRING[asset.settings.filtering] ?? 'linear',
|
|
155
157
|
generateMipmaps: asset.settings.generateMipmaps,
|
|
156
158
|
};
|
|
157
159
|
case AssetType.Audio:
|
|
@@ -196,11 +198,12 @@ function serializeMaterialOverrides(overrides) {
|
|
|
196
198
|
return result;
|
|
197
199
|
}
|
|
198
200
|
// ─── Generate default .stowmeta ─────────────────────────────────────────────
|
|
199
|
-
export function generateDefaultStowmeta(relativePath, type) {
|
|
201
|
+
export function generateDefaultStowmeta(relativePath, type, projectDefaults, imageDimensions) {
|
|
200
202
|
const fileName = relativePath.split('/').pop() ?? relativePath;
|
|
201
203
|
const resolvedType = type ?? detectAssetType(fileName);
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
+
// Use full relative path (minus extension) as stringId to avoid collisions across folders
|
|
205
|
+
const pathWithoutExt = relativePath.replace(/\.[^.]+$/, '');
|
|
206
|
+
const stringId = resolvedType === AssetType.MaterialSchema ? pathWithoutExt : pathWithoutExt.toLowerCase();
|
|
204
207
|
const base = {
|
|
205
208
|
version: 1,
|
|
206
209
|
stringId,
|
|
@@ -208,12 +211,39 @@ export function generateDefaultStowmeta(relativePath, type) {
|
|
|
208
211
|
pack: 'default',
|
|
209
212
|
};
|
|
210
213
|
switch (resolvedType) {
|
|
211
|
-
case AssetType.Texture2D:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
case AssetType.Texture2D: {
|
|
215
|
+
const textureDefaults = projectDefaults?.texture;
|
|
216
|
+
const isSmall = imageDimensions != null
|
|
217
|
+
&& imageDimensions.width <= 256
|
|
218
|
+
&& imageDimensions.height <= 256;
|
|
219
|
+
const defaultResize = isSmall ? 'full' : 'quarter';
|
|
220
|
+
return {
|
|
221
|
+
...base,
|
|
222
|
+
type: 'texture',
|
|
223
|
+
quality: textureDefaults?.quality ?? 'fastest',
|
|
224
|
+
resize: textureDefaults?.resize ?? defaultResize,
|
|
225
|
+
filtering: textureDefaults?.filtering ?? 'linear',
|
|
226
|
+
generateMipmaps: textureDefaults?.generateMipmaps ?? false,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case AssetType.Audio: {
|
|
230
|
+
const audioDefaults = projectDefaults?.audio;
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
type: 'audio',
|
|
234
|
+
aacQuality: audioDefaults?.aacQuality ?? 'medium',
|
|
235
|
+
sampleRate: audioDefaults?.sampleRate ?? 'auto',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case AssetType.StaticMesh: {
|
|
239
|
+
const meshDefaults = projectDefaults?.mesh;
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
type: 'staticMesh',
|
|
243
|
+
dracoQuality: meshDefaults?.dracoQuality ?? 'balanced',
|
|
244
|
+
materialOverrides: {},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
217
247
|
case AssetType.SkinnedMesh:
|
|
218
248
|
return { ...base, type: 'skinnedMesh', materialOverrides: {} };
|
|
219
249
|
case AssetType.AnimationClip:
|
|
@@ -228,23 +258,32 @@ export function generateDefaultStowmeta(relativePath, type) {
|
|
|
228
258
|
}
|
|
229
259
|
// ─── GLB Inline Child Helpers ───────────────────────────────────────────────
|
|
230
260
|
/** Generate a default inline child entry for a GLB container. */
|
|
231
|
-
export function generateDefaultGlbChild(name, childType) {
|
|
261
|
+
export function generateDefaultGlbChild(name, childType, projectDefaults) {
|
|
232
262
|
const baseName = name.replace(/\.[^.]+$/, '');
|
|
233
263
|
const stringId = childType === 'materialSchema' ? baseName : baseName.toLowerCase();
|
|
234
264
|
const child = { name, childType, stringId };
|
|
235
265
|
switch (childType) {
|
|
236
|
-
case 'texture':
|
|
237
|
-
|
|
238
|
-
child.
|
|
239
|
-
child.
|
|
266
|
+
case 'texture': {
|
|
267
|
+
const textureDefaults = projectDefaults?.texture;
|
|
268
|
+
child.quality = textureDefaults?.quality ?? 'fastest';
|
|
269
|
+
child.resize = textureDefaults?.resize ?? 'quarter';
|
|
270
|
+
child.filtering = textureDefaults?.filtering ?? 'linear';
|
|
271
|
+
child.generateMipmaps = textureDefaults?.generateMipmaps ?? false;
|
|
240
272
|
break;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
273
|
+
}
|
|
274
|
+
case 'audio': {
|
|
275
|
+
const audioDefaults = projectDefaults?.audio;
|
|
276
|
+
child.aacQuality = audioDefaults?.aacQuality ?? 'medium';
|
|
277
|
+
child.sampleRate = audioDefaults?.sampleRate ?? 'auto';
|
|
244
278
|
break;
|
|
245
|
-
|
|
279
|
+
}
|
|
280
|
+
case 'staticMesh': {
|
|
281
|
+
const meshDefaults = projectDefaults?.mesh;
|
|
282
|
+
child.dracoQuality = meshDefaults?.dracoQuality ?? 'balanced';
|
|
283
|
+
child.materialOverrides = {};
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
246
286
|
case 'skinnedMesh':
|
|
247
|
-
child.dracoQuality = 'balanced';
|
|
248
287
|
child.materialOverrides = {};
|
|
249
288
|
break;
|
|
250
289
|
case 'animationClip':
|
|
@@ -269,6 +308,7 @@ export function glbChildToAssetSettings(child) {
|
|
|
269
308
|
...base,
|
|
270
309
|
quality: KTX2_QUALITY_STRINGS[child.quality ?? 'fastest'] ?? KTX2Quality.Fastest,
|
|
271
310
|
resize: TEXTURE_RESIZE_STRINGS[child.resize ?? 'full'] ?? TextureResize.Full,
|
|
311
|
+
filtering: TEXTURE_FILTER_STRINGS[child.filtering ?? 'linear'] ?? TextureFilterMode.Linear,
|
|
272
312
|
generateMipmaps: child.generateMipmaps ?? false,
|
|
273
313
|
},
|
|
274
314
|
};
|
|
@@ -342,6 +382,7 @@ export function assetSettingsToGlbChild(name, childType, stringId, settings, exi
|
|
|
342
382
|
case 'texture':
|
|
343
383
|
child.quality = KTX2_QUALITY_TO_STRING[settings.quality] ?? 'fastest';
|
|
344
384
|
child.resize = TEXTURE_RESIZE_TO_STRING[settings.resize] ?? 'full';
|
|
385
|
+
child.filtering = TEXTURE_FILTER_TO_STRING[settings.filtering] ?? 'linear';
|
|
345
386
|
child.generateMipmaps = settings.generateMipmaps;
|
|
346
387
|
break;
|
|
347
388
|
case 'audio':
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ThumbnailManifestEntry {
|
|
2
|
+
format: 'png' | 'webp' | 'webm';
|
|
3
|
+
sourceSize: number;
|
|
4
|
+
settingsHash: string;
|
|
5
|
+
materialDepsHash: string;
|
|
6
|
+
}
|
|
7
|
+
export type ThumbnailManifest = Record<string, ThumbnailManifestEntry>;
|
|
8
|
+
export declare function readManifest(srcArtDir: string): Promise<ThumbnailManifest>;
|
|
9
|
+
export declare function writeManifest(srcArtDir: string, manifest: ThumbnailManifest): Promise<void>;
|
|
10
|
+
/** Hash the settings fields that affect thumbnail appearance. */
|
|
11
|
+
export declare function computeSettingsHash(settings: Record<string, unknown>): string;
|
|
12
|
+
/**
|
|
13
|
+
* Compute a hash over material dependencies for mesh/animation assets.
|
|
14
|
+
* Covers materialOverrides → referenced material asset settings + their texture dependencies.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeMaterialDepsHash(settings: Record<string, unknown>, allAssets: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
stringId: string;
|
|
19
|
+
sourceSize: number;
|
|
20
|
+
settings: Record<string, unknown>;
|
|
21
|
+
}>): string;
|
|
22
|
+
export declare function isThumbnailCacheValid(entry: ThumbnailManifestEntry | undefined, sourceSize: number, settingsHash: string, materialDepsHash: string): boolean;
|
|
23
|
+
export declare function writeThumbnailFile(srcArtDir: string, stringId: string, format: 'png' | 'webp' | 'webm', data: Buffer): Promise<void>;
|
|
24
|
+
export declare function readThumbnailFile(srcArtDir: string, stringId: string, format: 'png' | 'webp' | 'webm'): Promise<Buffer | null>;
|
|
25
|
+
/** Clean up thumbnail cache directory. */
|
|
26
|
+
export declare function cleanThumbnailCache(srcArtDir: string): Promise<{
|
|
27
|
+
deleted: number;
|
|
28
|
+
freedBytes: number;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side thumbnail cache.
|
|
3
|
+
* Stores captured thumbnails in `.stow-thumbnails/` to skip re-capture for unchanged assets.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'node:fs/promises';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as crypto from 'node:crypto';
|
|
8
|
+
const CACHE_DIR = '.stow-thumbnails';
|
|
9
|
+
const MANIFEST_FILE = 'manifest.json';
|
|
10
|
+
function cacheDir(srcArtDir) {
|
|
11
|
+
return path.join(srcArtDir, CACHE_DIR);
|
|
12
|
+
}
|
|
13
|
+
function manifestPath(srcArtDir) {
|
|
14
|
+
return path.join(cacheDir(srcArtDir), MANIFEST_FILE);
|
|
15
|
+
}
|
|
16
|
+
function thumbnailFileName(stringId, format) {
|
|
17
|
+
// Use a hash of stringId to avoid filesystem issues with slashes/special chars
|
|
18
|
+
const hash = crypto.createHash('sha256').update(stringId).digest('hex').slice(0, 16);
|
|
19
|
+
return `${hash}.${format}`;
|
|
20
|
+
}
|
|
21
|
+
export async function readManifest(srcArtDir) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await fs.readFile(manifestPath(srcArtDir), 'utf-8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function writeManifest(srcArtDir, manifest) {
|
|
31
|
+
const dir = cacheDir(srcArtDir);
|
|
32
|
+
await fs.mkdir(dir, { recursive: true });
|
|
33
|
+
await fs.writeFile(manifestPath(srcArtDir), JSON.stringify(manifest, null, 2));
|
|
34
|
+
}
|
|
35
|
+
/** Hash the settings fields that affect thumbnail appearance. */
|
|
36
|
+
export function computeSettingsHash(settings) {
|
|
37
|
+
// Only include settings that affect visual output
|
|
38
|
+
const relevant = {};
|
|
39
|
+
for (const key of ['quality', 'resize', 'generateMipmaps', 'dracoQuality', 'materialConfig', 'targetMeshId', 'materialOverrides']) {
|
|
40
|
+
if (settings[key] !== undefined)
|
|
41
|
+
relevant[key] = settings[key];
|
|
42
|
+
}
|
|
43
|
+
return crypto.createHash('sha256').update(JSON.stringify(relevant)).digest('hex').slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute a hash over material dependencies for mesh/animation assets.
|
|
47
|
+
* Covers materialOverrides → referenced material asset settings + their texture dependencies.
|
|
48
|
+
*/
|
|
49
|
+
export function computeMaterialDepsHash(settings, allAssets) {
|
|
50
|
+
const overrides = settings.materialOverrides;
|
|
51
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
52
|
+
return '';
|
|
53
|
+
const parts = [];
|
|
54
|
+
for (const [idx, matId] of Object.entries(overrides)) {
|
|
55
|
+
if (!matId)
|
|
56
|
+
continue;
|
|
57
|
+
const matAsset = allAssets.find(a => a.id === matId);
|
|
58
|
+
if (!matAsset)
|
|
59
|
+
continue;
|
|
60
|
+
// Include material's settings and sourceSize
|
|
61
|
+
parts.push(`${idx}:${matId}:${matAsset.sourceSize}:${JSON.stringify(matAsset.settings.materialConfig ?? {})}`);
|
|
62
|
+
// Also hash texture assets referenced by material properties
|
|
63
|
+
const matConfig = matAsset.settings.materialConfig;
|
|
64
|
+
if (matConfig?.properties) {
|
|
65
|
+
for (const prop of matConfig.properties) {
|
|
66
|
+
if (prop.type === 'texture' && prop.value) {
|
|
67
|
+
const texAsset = allAssets.find(a => a.id === prop.value);
|
|
68
|
+
if (texAsset)
|
|
69
|
+
parts.push(`tex:${prop.value}:${texAsset.sourceSize}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// For animation clips, also include the target mesh
|
|
75
|
+
const targetMeshId = settings.targetMeshId;
|
|
76
|
+
if (targetMeshId) {
|
|
77
|
+
const meshAsset = allAssets.find(a => a.id === targetMeshId);
|
|
78
|
+
if (meshAsset) {
|
|
79
|
+
parts.push(`mesh:${targetMeshId}:${meshAsset.sourceSize}:${computeSettingsHash(meshAsset.settings)}`);
|
|
80
|
+
// Recurse: include the mesh's own material deps
|
|
81
|
+
const meshDeps = computeMaterialDepsHash(meshAsset.settings, allAssets);
|
|
82
|
+
if (meshDeps)
|
|
83
|
+
parts.push(`meshDeps:${meshDeps}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (parts.length === 0)
|
|
87
|
+
return '';
|
|
88
|
+
return crypto.createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
|
|
89
|
+
}
|
|
90
|
+
export function isThumbnailCacheValid(entry, sourceSize, settingsHash, materialDepsHash) {
|
|
91
|
+
if (!entry)
|
|
92
|
+
return false;
|
|
93
|
+
return entry.sourceSize === sourceSize &&
|
|
94
|
+
entry.settingsHash === settingsHash &&
|
|
95
|
+
entry.materialDepsHash === materialDepsHash;
|
|
96
|
+
}
|
|
97
|
+
export async function writeThumbnailFile(srcArtDir, stringId, format, data) {
|
|
98
|
+
const dir = cacheDir(srcArtDir);
|
|
99
|
+
await fs.mkdir(dir, { recursive: true });
|
|
100
|
+
await fs.writeFile(path.join(dir, thumbnailFileName(stringId, format)), data);
|
|
101
|
+
}
|
|
102
|
+
export async function readThumbnailFile(srcArtDir, stringId, format) {
|
|
103
|
+
try {
|
|
104
|
+
return await fs.readFile(path.join(cacheDir(srcArtDir), thumbnailFileName(stringId, format)));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Clean up thumbnail cache directory. */
|
|
111
|
+
export async function cleanThumbnailCache(srcArtDir) {
|
|
112
|
+
const dir = cacheDir(srcArtDir);
|
|
113
|
+
let deleted = 0;
|
|
114
|
+
let freedBytes = 0;
|
|
115
|
+
try {
|
|
116
|
+
const manifest = await readManifest(srcArtDir);
|
|
117
|
+
const validFiles = new Set();
|
|
118
|
+
for (const [stringId, entry] of Object.entries(manifest)) {
|
|
119
|
+
validFiles.add(thumbnailFileName(stringId, entry.format));
|
|
120
|
+
}
|
|
121
|
+
validFiles.add(MANIFEST_FILE);
|
|
122
|
+
const entries = await fs.readdir(dir);
|
|
123
|
+
for (const name of entries) {
|
|
124
|
+
if (!validFiles.has(name)) {
|
|
125
|
+
const fullPath = path.join(dir, name);
|
|
126
|
+
const stat = await fs.stat(fullPath);
|
|
127
|
+
await fs.unlink(fullPath);
|
|
128
|
+
deleted++;
|
|
129
|
+
freedBytes += stat.size;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Directory doesn't exist yet — nothing to clean
|
|
135
|
+
}
|
|
136
|
+
return { deleted, freedBytes };
|
|
137
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface AssetsPackage {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
author: string;
|
|
5
|
+
description: string;
|
|
6
|
+
tags: string[];
|
|
7
|
+
bucket: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RegistryAsset {
|
|
10
|
+
/** Unique string identifier for this asset */
|
|
11
|
+
stringId: string;
|
|
12
|
+
/** Asset type: texture, staticMesh, skinnedMesh, audio, materialSchema, animationClip, glbContainer */
|
|
13
|
+
type: string;
|
|
14
|
+
/** Relative file path within the version directory */
|
|
15
|
+
file: string;
|
|
16
|
+
/** All files needed for this asset (source + stowmeta + stowmat) */
|
|
17
|
+
files: string[];
|
|
18
|
+
/** Source file size in bytes */
|
|
19
|
+
size: number;
|
|
20
|
+
/** User-defined tags from stowmeta */
|
|
21
|
+
tags: string[];
|
|
22
|
+
/** Direct dependency stringIds (mesh→material, material→texture, anim→mesh) */
|
|
23
|
+
dependencies: string[];
|
|
24
|
+
/** Whether a thumbnail was uploaded for this asset */
|
|
25
|
+
thumbnail?: boolean;
|
|
26
|
+
/** Thumbnail format: 'png' (default) or 'webm' for animated previews */
|
|
27
|
+
thumbnailFormat?: 'png' | 'webp' | 'webm';
|
|
28
|
+
/** Texture filtering mode — 'nearest' for pixel art, 'linear' (default) for smooth */
|
|
29
|
+
filtering?: 'linear' | 'nearest';
|
|
30
|
+
}
|
|
31
|
+
export interface RegistryVersion {
|
|
32
|
+
publishedAt: string;
|
|
33
|
+
fileCount: number;
|
|
34
|
+
totalSize: number;
|
|
35
|
+
assets: RegistryAsset[];
|
|
36
|
+
}
|
|
37
|
+
export interface RegistryPackage {
|
|
38
|
+
description: string;
|
|
39
|
+
author: string;
|
|
40
|
+
tags: string[];
|
|
41
|
+
latest: string;
|
|
42
|
+
versions: Record<string, RegistryVersion>;
|
|
43
|
+
/** Pack-level thumbnail filename (e.g. "thumbnail.webp"), uploaded during publish */
|
|
44
|
+
thumbnail?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Registry {
|
|
47
|
+
schemaVersion: number;
|
|
48
|
+
packages: Record<string, RegistryPackage>;
|
|
49
|
+
}
|
|
50
|
+
export declare function readAssetsPackage(dir: string): Promise<AssetsPackage | null>;
|
|
51
|
+
export declare function writeAssetsPackage(dir: string, pkg: AssetsPackage): Promise<void>;
|
|
52
|
+
export declare function initAssetsPackage(dir: string, config: {
|
|
53
|
+
name: string;
|
|
54
|
+
}): Promise<AssetsPackage>;
|
|
55
|
+
export declare function createEmptyRegistry(): Registry;
|
|
56
|
+
/**
|
|
57
|
+
* Given a set of requested stringIds and a version's asset list,
|
|
58
|
+
* returns the full set of stringIds needed (transitive closure).
|
|
59
|
+
* Deduplicates automatically — shared deps are only included once.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveTransitiveDeps(requestedIds: string[], assets: RegistryAsset[]): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Given resolved stringIds and a version's asset list,
|
|
64
|
+
* returns the deduplicated set of files needed to download.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveFiles(stringIds: string[], assets: RegistryAsset[]): string[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
4
|
+
const ASSETS_PACKAGE_FILE = 'assets-package.json';
|
|
5
|
+
const DEFAULT_BUCKET = 'venus-shared-assets-test';
|
|
6
|
+
// ─── Read / Write / Init ─────────────────────────────────────────────────────
|
|
7
|
+
export async function readAssetsPackage(dir) {
|
|
8
|
+
try {
|
|
9
|
+
const filePath = path.join(dir, ASSETS_PACKAGE_FILE);
|
|
10
|
+
const text = await fs.readFile(filePath, 'utf-8');
|
|
11
|
+
return JSON.parse(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function writeAssetsPackage(dir, pkg) {
|
|
18
|
+
const filePath = path.join(dir, ASSETS_PACKAGE_FILE);
|
|
19
|
+
await fs.writeFile(filePath, JSON.stringify(pkg, null, 2) + '\n');
|
|
20
|
+
}
|
|
21
|
+
export async function initAssetsPackage(dir, config) {
|
|
22
|
+
const pkg = {
|
|
23
|
+
name: config.name,
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
author: '',
|
|
26
|
+
description: '',
|
|
27
|
+
tags: [],
|
|
28
|
+
bucket: `gs://${DEFAULT_BUCKET}`,
|
|
29
|
+
};
|
|
30
|
+
await writeAssetsPackage(dir, pkg);
|
|
31
|
+
return pkg;
|
|
32
|
+
}
|
|
33
|
+
export function createEmptyRegistry() {
|
|
34
|
+
return { schemaVersion: 1, packages: {} };
|
|
35
|
+
}
|
|
36
|
+
// ─── Dependency Resolution ───────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Given a set of requested stringIds and a version's asset list,
|
|
39
|
+
* returns the full set of stringIds needed (transitive closure).
|
|
40
|
+
* Deduplicates automatically — shared deps are only included once.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveTransitiveDeps(requestedIds, assets) {
|
|
43
|
+
const assetMap = new Map();
|
|
44
|
+
for (const a of assets)
|
|
45
|
+
assetMap.set(a.stringId, a);
|
|
46
|
+
const resolved = new Set();
|
|
47
|
+
const queue = [...requestedIds];
|
|
48
|
+
while (queue.length > 0) {
|
|
49
|
+
const id = queue.pop();
|
|
50
|
+
if (resolved.has(id))
|
|
51
|
+
continue;
|
|
52
|
+
resolved.add(id);
|
|
53
|
+
const asset = assetMap.get(id);
|
|
54
|
+
if (!asset)
|
|
55
|
+
continue;
|
|
56
|
+
for (const dep of asset.dependencies) {
|
|
57
|
+
if (!resolved.has(dep))
|
|
58
|
+
queue.push(dep);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...resolved];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Given resolved stringIds and a version's asset list,
|
|
65
|
+
* returns the deduplicated set of files needed to download.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveFiles(stringIds, assets) {
|
|
68
|
+
const assetMap = new Map();
|
|
69
|
+
for (const a of assets)
|
|
70
|
+
assetMap.set(a.stringId, a);
|
|
71
|
+
const files = new Set();
|
|
72
|
+
for (const id of stringIds) {
|
|
73
|
+
const asset = assetMap.get(id);
|
|
74
|
+
if (!asset)
|
|
75
|
+
continue;
|
|
76
|
+
for (const f of asset.files)
|
|
77
|
+
files.add(f);
|
|
78
|
+
}
|
|
79
|
+
return [...files];
|
|
80
|
+
}
|
package/dist/cleanup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { readProjectConfig, scanDirectory } from './node-fs.js';
|
|
4
|
+
import { cleanThumbnailCache } from './app/thumbnail-cache.js';
|
|
4
5
|
export async function cleanupProject(projectDir, opts) {
|
|
5
6
|
const verbose = opts?.verbose ?? false;
|
|
6
7
|
const config = await readProjectConfig(projectDir);
|
|
@@ -27,11 +28,19 @@ export async function cleanupProject(projectDir, opts) {
|
|
|
27
28
|
console.log(` [delete] ${orphanPath} (orphaned meta)`);
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
+
// Clean orphaned thumbnail cache files
|
|
32
|
+
const thumbResult = await cleanThumbnailCache(config.srcArtDir);
|
|
33
|
+
if (thumbResult.deleted > 0) {
|
|
34
|
+
freedBytes += thumbResult.freedBytes;
|
|
35
|
+
if (verbose)
|
|
36
|
+
console.log(` [delete] ${thumbResult.deleted} orphaned thumbnail cache file(s)`);
|
|
37
|
+
}
|
|
38
|
+
if (deletedCaches + deletedMetas + thumbResult.deleted === 0) {
|
|
31
39
|
console.log('No orphaned files found.');
|
|
32
40
|
}
|
|
33
41
|
else {
|
|
34
|
-
|
|
42
|
+
const thumbNote = thumbResult.deleted > 0 ? `, ${thumbResult.deleted} thumbnail(s)` : '';
|
|
43
|
+
console.log(`Cleaned up ${deletedCaches} cache(s) and ${deletedMetas} meta(s)${thumbNote} (${(freedBytes / 1024).toFixed(0)} KB freed)`);
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
async function walkForOrphans(basePath, prefix, sourceFiles, matFiles, onOrphan) {
|