@series-inc/stowkit-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/app/blob-store.d.ts +9 -0
  2. package/dist/app/blob-store.js +42 -0
  3. package/dist/app/disk-project.d.ts +84 -0
  4. package/dist/app/disk-project.js +70 -0
  5. package/dist/app/process-cache.d.ts +10 -0
  6. package/dist/app/process-cache.js +126 -0
  7. package/dist/app/state.d.ts +38 -0
  8. package/dist/app/state.js +16 -0
  9. package/dist/app/stowmat-io.d.ts +6 -0
  10. package/dist/app/stowmat-io.js +48 -0
  11. package/dist/app/stowmeta-io.d.ts +14 -0
  12. package/dist/app/stowmeta-io.js +207 -0
  13. package/dist/cleanup.d.ts +3 -0
  14. package/dist/cleanup.js +72 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +148 -0
  17. package/dist/core/binary.d.ts +41 -0
  18. package/dist/core/binary.js +118 -0
  19. package/dist/core/constants.d.ts +64 -0
  20. package/dist/core/constants.js +65 -0
  21. package/dist/core/path.d.ts +3 -0
  22. package/dist/core/path.js +27 -0
  23. package/dist/core/types.d.ts +204 -0
  24. package/dist/core/types.js +76 -0
  25. package/dist/encoders/aac-encoder.d.ts +12 -0
  26. package/dist/encoders/aac-encoder.js +179 -0
  27. package/dist/encoders/basis-encoder.d.ts +15 -0
  28. package/dist/encoders/basis-encoder.js +116 -0
  29. package/dist/encoders/draco-encoder.d.ts +11 -0
  30. package/dist/encoders/draco-encoder.js +155 -0
  31. package/dist/encoders/fbx-loader.d.ts +4 -0
  32. package/dist/encoders/fbx-loader.js +540 -0
  33. package/dist/encoders/image-decoder.d.ts +13 -0
  34. package/dist/encoders/image-decoder.js +33 -0
  35. package/dist/encoders/interfaces.d.ts +105 -0
  36. package/dist/encoders/interfaces.js +1 -0
  37. package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
  38. package/dist/encoders/skinned-mesh-builder.js +135 -0
  39. package/dist/format/metadata.d.ts +18 -0
  40. package/dist/format/metadata.js +381 -0
  41. package/dist/format/packer.d.ts +8 -0
  42. package/dist/format/packer.js +87 -0
  43. package/dist/index.d.ts +28 -0
  44. package/dist/index.js +35 -0
  45. package/dist/init.d.ts +1 -0
  46. package/dist/init.js +73 -0
  47. package/dist/node-fs.d.ts +22 -0
  48. package/dist/node-fs.js +148 -0
  49. package/dist/orchestrator.d.ts +20 -0
  50. package/dist/orchestrator.js +301 -0
  51. package/dist/pipeline.d.ts +23 -0
  52. package/dist/pipeline.js +354 -0
  53. package/dist/server.d.ts +9 -0
  54. package/dist/server.js +859 -0
  55. package/package.json +35 -0
  56. package/skill.md +211 -0
@@ -0,0 +1,9 @@
1
+ export declare const BlobStore: {
2
+ setSource(id: string, data: Uint8Array): void;
3
+ getSource(id: string): Uint8Array | undefined;
4
+ setProcessed(id: string, data: Uint8Array): void;
5
+ getProcessed(id: string): Uint8Array | undefined;
6
+ remove(id: string): void;
7
+ renameAll(oldId: string, newId: string): void;
8
+ clear(): void;
9
+ };
@@ -0,0 +1,42 @@
1
+ const sourceBlobs = new Map();
2
+ const processedBlobs = new Map();
3
+ export const BlobStore = {
4
+ setSource(id, data) {
5
+ sourceBlobs.set(id, data);
6
+ },
7
+ getSource(id) {
8
+ return sourceBlobs.get(id);
9
+ },
10
+ setProcessed(id, data) {
11
+ processedBlobs.set(id, data);
12
+ },
13
+ getProcessed(id) {
14
+ return processedBlobs.get(id);
15
+ },
16
+ remove(id) {
17
+ sourceBlobs.delete(id);
18
+ processedBlobs.delete(id);
19
+ },
20
+ renameAll(oldId, newId) {
21
+ const prefix = `${oldId}:`;
22
+ for (const store of [sourceBlobs, processedBlobs]) {
23
+ const moves = [];
24
+ for (const [key, data] of store) {
25
+ if (key === oldId) {
26
+ moves.push([key, newId, data]);
27
+ }
28
+ else if (key.startsWith(prefix)) {
29
+ moves.push([key, `${newId}${key.slice(oldId.length)}`, data]);
30
+ }
31
+ }
32
+ for (const [oldKey, newKey, data] of moves) {
33
+ store.delete(oldKey);
34
+ store.set(newKey, data);
35
+ }
36
+ }
37
+ },
38
+ clear() {
39
+ sourceBlobs.clear();
40
+ processedBlobs.clear();
41
+ },
42
+ };
@@ -0,0 +1,84 @@
1
+ import type { KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
2
+ export interface PackConfig {
3
+ name: string;
4
+ }
5
+ export interface FelicityProject {
6
+ srcArtDir: string;
7
+ name?: string;
8
+ cdnAssetsPath?: string;
9
+ prefabsPath?: string;
10
+ packs?: PackConfig[];
11
+ }
12
+ export interface FileSnapshot {
13
+ relativePath: string;
14
+ size: number;
15
+ lastModified: number;
16
+ }
17
+ export interface StowMetaCache {
18
+ sourceSize: number;
19
+ sourceLastModified: number;
20
+ settingsHash: string;
21
+ }
22
+ interface StowMetaBase {
23
+ version: 1;
24
+ type: string;
25
+ stringId: string;
26
+ tags: string[];
27
+ pack?: string;
28
+ cache?: StowMetaCache;
29
+ }
30
+ export interface StowMetaTexture extends StowMetaBase {
31
+ type: 'texture';
32
+ quality: string;
33
+ resize: string;
34
+ generateMipmaps: boolean;
35
+ }
36
+ export interface StowMetaAudio extends StowMetaBase {
37
+ type: 'audio';
38
+ aacQuality: string;
39
+ sampleRate: string;
40
+ }
41
+ export interface StowMetaStaticMesh extends StowMetaBase {
42
+ type: 'staticMesh';
43
+ dracoQuality: string;
44
+ materialOverrides: Record<string, string | null>;
45
+ }
46
+ export interface StowMetaSkinnedMesh extends StowMetaBase {
47
+ type: 'skinnedMesh';
48
+ materialOverrides: Record<string, string | null>;
49
+ }
50
+ export interface StowMetaAnimationClip extends StowMetaBase {
51
+ type: 'animationClip';
52
+ targetMeshId: string | null;
53
+ }
54
+ export interface StowMetaMaterialSchema extends StowMetaBase {
55
+ type: 'materialSchema';
56
+ }
57
+ export type StowMeta = StowMetaTexture | StowMetaAudio | StowMetaStaticMesh | StowMetaSkinnedMesh | StowMetaAnimationClip | StowMetaMaterialSchema;
58
+ export interface StowMatProperty {
59
+ fieldName: string;
60
+ fieldType: string;
61
+ previewFlag: string;
62
+ value: [number, number, number, number];
63
+ textureAsset: string | null;
64
+ }
65
+ export interface StowMat {
66
+ version: 1;
67
+ schemaName: string;
68
+ properties: StowMatProperty[];
69
+ }
70
+ export declare const KTX2_QUALITY_STRINGS: Record<string, KTX2Quality>;
71
+ export declare const KTX2_QUALITY_TO_STRING: Record<number, string>;
72
+ export declare const TEXTURE_RESIZE_STRINGS: Record<string, TextureResize>;
73
+ export declare const TEXTURE_RESIZE_TO_STRING: Record<number, string>;
74
+ export declare const DRACO_QUALITY_STRINGS: Record<string, DracoQualityPreset>;
75
+ export declare const DRACO_QUALITY_TO_STRING: Record<number, string>;
76
+ export declare const AAC_QUALITY_STRINGS: Record<string, AacQuality>;
77
+ export declare const AAC_QUALITY_TO_STRING: Record<number, string>;
78
+ export declare const AUDIO_SAMPLE_RATE_STRINGS: Record<string, AudioSampleRate>;
79
+ export declare const AUDIO_SAMPLE_RATE_TO_STRING: Record<number, string>;
80
+ export declare const MATERIAL_FIELD_TYPE_STRINGS: Record<string, MaterialFieldType>;
81
+ export declare const MATERIAL_FIELD_TYPE_TO_STRING: Record<number, string>;
82
+ export declare const PREVIEW_FLAG_STRINGS: Record<string, PreviewPropertyFlag>;
83
+ export declare const PREVIEW_FLAG_TO_STRING: Record<number, string>;
84
+ export {};
@@ -0,0 +1,70 @@
1
+ // ─── Enum String Mapping ─────────────────────────────────────────────────────
2
+ export const KTX2_QUALITY_STRINGS = {
3
+ fastest: 0,
4
+ fast: 1,
5
+ normal: 2,
6
+ high: 3,
7
+ best: 4,
8
+ };
9
+ export const KTX2_QUALITY_TO_STRING = {
10
+ 0: 'fastest', 1: 'fast', 2: 'normal', 3: 'high', 4: 'best',
11
+ };
12
+ export const TEXTURE_RESIZE_STRINGS = {
13
+ full: 0,
14
+ half: 1,
15
+ quarter: 2,
16
+ eighth: 3,
17
+ };
18
+ export const TEXTURE_RESIZE_TO_STRING = {
19
+ 0: 'full', 1: 'half', 2: 'quarter', 3: 'eighth',
20
+ };
21
+ export const DRACO_QUALITY_STRINGS = {
22
+ fast: 0,
23
+ balanced: 1,
24
+ high: 2,
25
+ maximum: 3,
26
+ };
27
+ export const DRACO_QUALITY_TO_STRING = {
28
+ 0: 'fast', 1: 'balanced', 2: 'high', 3: 'maximum',
29
+ };
30
+ export const AAC_QUALITY_STRINGS = {
31
+ lowest: 0,
32
+ low: 1,
33
+ medium: 2,
34
+ high: 3,
35
+ best: 4,
36
+ };
37
+ export const AAC_QUALITY_TO_STRING = {
38
+ 0: 'lowest', 1: 'low', 2: 'medium', 3: 'high', 4: 'best',
39
+ };
40
+ export const AUDIO_SAMPLE_RATE_STRINGS = {
41
+ auto: 0,
42
+ '48000': 48000,
43
+ '44100': 44100,
44
+ '22050': 22050,
45
+ '11025': 11025,
46
+ };
47
+ export const AUDIO_SAMPLE_RATE_TO_STRING = {
48
+ 0: 'auto', 48000: '48000', 44100: '44100', 22050: '22050', 11025: '11025',
49
+ };
50
+ export const MATERIAL_FIELD_TYPE_STRINGS = {
51
+ texture: 0,
52
+ color: 1,
53
+ float: 2,
54
+ vec2: 3,
55
+ vec3: 4,
56
+ vec4: 5,
57
+ int: 6,
58
+ };
59
+ export const MATERIAL_FIELD_TYPE_TO_STRING = {
60
+ 0: 'texture', 1: 'color', 2: 'float', 3: 'vec2', 4: 'vec3', 5: 'vec4', 6: 'int',
61
+ };
62
+ export const PREVIEW_FLAG_STRINGS = {
63
+ none: 0,
64
+ mainTex: 1,
65
+ tint: 2,
66
+ alphaTest: 3,
67
+ };
68
+ export const PREVIEW_FLAG_TO_STRING = {
69
+ 0: 'none', 1: 'mainTex', 2: 'tint', 3: 'alphaTest',
70
+ };
@@ -0,0 +1,10 @@
1
+ import type { AssetSettings } from './state.js';
2
+ import type { StowMeta, StowMetaCache, FileSnapshot } from './disk-project.js';
3
+ import type { AssetType } from '../core/types.js';
4
+ export declare function cachePath(relativePath: string): string;
5
+ export declare function writeCacheBlobs(srcArtDir: string, relativePath: string, entries: Map<string, Uint8Array>): Promise<void>;
6
+ export declare function readCacheBlobs(srcArtDir: string, relativePath: string): Promise<Map<string, Uint8Array> | null>;
7
+ export declare function deleteCacheFile(srcArtDir: string, relativePath: string): Promise<void>;
8
+ export declare function computeSettingsHash(type: AssetType, settings: AssetSettings): string;
9
+ export declare function isCacheValid(meta: StowMeta, sourceSnapshot: FileSnapshot, type: AssetType, settings: AssetSettings): boolean;
10
+ export declare function buildCacheStamp(sourceSnapshot: FileSnapshot, type: AssetType, settings: AssetSettings): StowMetaCache;
@@ -0,0 +1,126 @@
1
+ import { readFile, writeFile, deleteFile } from '../node-fs.js';
2
+ // ─── Cache File Path ─────────────────────────────────────────────────────────
3
+ export function cachePath(relativePath) {
4
+ return `${relativePath}.stowcache`;
5
+ }
6
+ // ─── Binary Cache Format ─────────────────────────────────────────────────────
7
+ export async function writeCacheBlobs(srcArtDir, relativePath, entries) {
8
+ try {
9
+ const encoder = new TextEncoder();
10
+ let totalSize = 4;
11
+ const encodedKeys = [];
12
+ for (const [key, data] of entries) {
13
+ const keyBytes = encoder.encode(key);
14
+ encodedKeys.push(keyBytes);
15
+ totalSize += 2 + keyBytes.length + 4 + data.length;
16
+ }
17
+ const buffer = new ArrayBuffer(totalSize);
18
+ const view = new DataView(buffer);
19
+ const bytes = new Uint8Array(buffer);
20
+ let offset = 0;
21
+ view.setUint32(offset, entries.size, true);
22
+ offset += 4;
23
+ let keyIdx = 0;
24
+ for (const [_key, data] of entries) {
25
+ const keyBytes = encodedKeys[keyIdx++];
26
+ view.setUint16(offset, keyBytes.length, true);
27
+ offset += 2;
28
+ bytes.set(keyBytes, offset);
29
+ offset += keyBytes.length;
30
+ view.setUint32(offset, data.length, true);
31
+ offset += 4;
32
+ bytes.set(data, offset);
33
+ offset += data.length;
34
+ }
35
+ await writeFile(srcArtDir, cachePath(relativePath), bytes);
36
+ }
37
+ catch (err) {
38
+ console.warn(`[cache] Failed to write cache for ${relativePath}:`, err);
39
+ }
40
+ }
41
+ export async function readCacheBlobs(srcArtDir, relativePath) {
42
+ try {
43
+ const data = await readFile(srcArtDir, cachePath(relativePath));
44
+ if (!data || data.length < 4)
45
+ return null;
46
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
47
+ const decoder = new TextDecoder();
48
+ const result = new Map();
49
+ let offset = 0;
50
+ const entryCount = view.getUint32(offset, true);
51
+ offset += 4;
52
+ for (let i = 0; i < entryCount; i++) {
53
+ if (offset + 2 > data.length)
54
+ return null;
55
+ const keyLen = view.getUint16(offset, true);
56
+ offset += 2;
57
+ if (offset + keyLen > data.length)
58
+ return null;
59
+ const key = decoder.decode(data.subarray(offset, offset + keyLen));
60
+ offset += keyLen;
61
+ if (offset + 4 > data.length)
62
+ return null;
63
+ const dataLen = view.getUint32(offset, true);
64
+ offset += 4;
65
+ if (offset + dataLen > data.length)
66
+ return null;
67
+ const entryData = data.slice(offset, offset + dataLen);
68
+ offset += dataLen;
69
+ result.set(key, entryData);
70
+ }
71
+ return result;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ export async function deleteCacheFile(srcArtDir, relativePath) {
78
+ try {
79
+ await deleteFile(srcArtDir, cachePath(relativePath));
80
+ }
81
+ catch {
82
+ // Ignore
83
+ }
84
+ }
85
+ // ─── Settings Hash ───────────────────────────────────────────────────────────
86
+ export function computeSettingsHash(type, settings) {
87
+ const parts = [String(type)];
88
+ switch (type) {
89
+ case 2: // Texture2D
90
+ parts.push(`q=${settings.quality}`, `r=${settings.resize}`, `m=${settings.generateMipmaps}`);
91
+ break;
92
+ case 3: // Audio
93
+ parts.push(`aq=${settings.aacQuality}`, `sr=${settings.audioSampleRate}`);
94
+ break;
95
+ case 1: // StaticMesh
96
+ parts.push(`dq=${settings.dracoQuality}`);
97
+ break;
98
+ case 5: // SkinnedMesh
99
+ parts.push('v2');
100
+ break;
101
+ case 6: // AnimationClip
102
+ parts.push('v2');
103
+ break;
104
+ }
105
+ return parts.join('|');
106
+ }
107
+ // ─── Cache Validity ──────────────────────────────────────────────────────────
108
+ export function isCacheValid(meta, sourceSnapshot, type, settings) {
109
+ if (!meta.cache)
110
+ return false;
111
+ if (meta.cache.sourceSize !== sourceSnapshot.size)
112
+ return false;
113
+ if (meta.cache.sourceLastModified !== sourceSnapshot.lastModified)
114
+ return false;
115
+ const currentHash = computeSettingsHash(type, settings);
116
+ if (meta.cache.settingsHash !== currentHash)
117
+ return false;
118
+ return true;
119
+ }
120
+ export function buildCacheStamp(sourceSnapshot, type, settings) {
121
+ return {
122
+ sourceSize: sourceSnapshot.size,
123
+ sourceLastModified: sourceSnapshot.lastModified,
124
+ settingsHash: computeSettingsHash(type, settings),
125
+ };
126
+ }
@@ -0,0 +1,38 @@
1
+ import type { AssetType, TextureMetadata, AudioMetadata, MeshMetadata, AnimationClipMetadata, SkinnedMeshMetadata, KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate, MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
2
+ export interface MaterialProperty {
3
+ fieldName: string;
4
+ fieldType: MaterialFieldType;
5
+ previewFlag: PreviewPropertyFlag;
6
+ value: [number, number, number, number];
7
+ textureAssetId: string | null;
8
+ }
9
+ export interface MaterialConfig {
10
+ schemaId: string;
11
+ properties: MaterialProperty[];
12
+ }
13
+ export interface AssetSettings {
14
+ quality: KTX2Quality;
15
+ resize: TextureResize;
16
+ generateMipmaps: boolean;
17
+ tags: string[];
18
+ dracoQuality: DracoQualityPreset;
19
+ aacQuality: AacQuality;
20
+ audioSampleRate: AudioSampleRate;
21
+ targetMeshId: string | null;
22
+ materialConfig: MaterialConfig;
23
+ materialOverrides: Record<number, string | null>;
24
+ pack: string;
25
+ }
26
+ export declare function defaultAssetSettings(): AssetSettings;
27
+ export interface ProjectAsset {
28
+ id: string;
29
+ fileName: string;
30
+ stringId: string;
31
+ type: AssetType;
32
+ status: 'loading' | 'pending' | 'processing' | 'ready' | 'error';
33
+ error?: string;
34
+ settings: AssetSettings;
35
+ metadata?: TextureMetadata | AudioMetadata | MeshMetadata | AnimationClipMetadata | SkinnedMeshMetadata;
36
+ sourceSize: number;
37
+ processedSize: number;
38
+ }
@@ -0,0 +1,16 @@
1
+ import { KTX2Quality as KQ, TextureResize as TR, DracoQualityPreset as DQ, AacQuality as AQ, AudioSampleRate as ASR, } from '../core/types.js';
2
+ export function defaultAssetSettings() {
3
+ return {
4
+ quality: KQ.Fastest,
5
+ resize: TR.Full,
6
+ generateMipmaps: false,
7
+ tags: [],
8
+ dracoQuality: DQ.Balanced,
9
+ aacQuality: AQ.Medium,
10
+ audioSampleRate: ASR.Auto,
11
+ targetMeshId: null,
12
+ materialConfig: { schemaId: '', properties: [] },
13
+ materialOverrides: {},
14
+ pack: 'default',
15
+ };
16
+ }
@@ -0,0 +1,6 @@
1
+ import type { MaterialConfig } from './state.js';
2
+ import type { StowMat } from './disk-project.js';
3
+ export declare function readStowmat(srcArtDir: string, relativePath: string): Promise<StowMat | null>;
4
+ export declare function writeStowmat(srcArtDir: string, relativePath: string, mat: StowMat): Promise<void>;
5
+ export declare function materialConfigToStowmat(config: MaterialConfig): StowMat;
6
+ export declare function stowmatToMaterialConfig(mat: StowMat): MaterialConfig;
@@ -0,0 +1,48 @@
1
+ import { MATERIAL_FIELD_TYPE_STRINGS, MATERIAL_FIELD_TYPE_TO_STRING, PREVIEW_FLAG_STRINGS, PREVIEW_FLAG_TO_STRING, } from './disk-project.js';
2
+ import { MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
3
+ import { readTextFile, writeFile } from '../node-fs.js';
4
+ export async function readStowmat(srcArtDir, relativePath) {
5
+ const text = await readTextFile(srcArtDir, relativePath);
6
+ if (!text)
7
+ return null;
8
+ try {
9
+ return JSON.parse(text);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export async function writeStowmat(srcArtDir, relativePath, mat) {
16
+ try {
17
+ const json = JSON.stringify(mat, null, 2);
18
+ await writeFile(srcArtDir, relativePath, json);
19
+ }
20
+ catch (err) {
21
+ console.warn(`[stowmat] Failed to write ${relativePath}:`, err);
22
+ }
23
+ }
24
+ export function materialConfigToStowmat(config) {
25
+ return {
26
+ version: 1,
27
+ schemaName: config.schemaId,
28
+ properties: config.properties.map((prop) => ({
29
+ fieldName: prop.fieldName,
30
+ fieldType: MATERIAL_FIELD_TYPE_TO_STRING[prop.fieldType] ?? 'color',
31
+ previewFlag: PREVIEW_FLAG_TO_STRING[prop.previewFlag] ?? 'none',
32
+ value: [...prop.value],
33
+ textureAsset: prop.textureAssetId,
34
+ })),
35
+ };
36
+ }
37
+ export function stowmatToMaterialConfig(mat) {
38
+ return {
39
+ schemaId: mat.schemaName ?? '',
40
+ properties: (mat.properties ?? []).map((prop) => ({
41
+ fieldName: prop.fieldName ?? '',
42
+ fieldType: MATERIAL_FIELD_TYPE_STRINGS[prop.fieldType] ?? MaterialFieldType.Color,
43
+ previewFlag: PREVIEW_FLAG_STRINGS[prop.previewFlag] ?? PreviewPropertyFlag.None,
44
+ value: prop.value ?? [1, 1, 1, 1],
45
+ textureAssetId: prop.textureAsset ?? null,
46
+ })),
47
+ };
48
+ }
@@ -0,0 +1,14 @@
1
+ import type { AssetSettings } from './state.js';
2
+ import { AssetType } from '../core/types.js';
3
+ import type { ProjectAsset } from './state.js';
4
+ import type { StowMeta } from './disk-project.js';
5
+ export declare function detectAssetType(fileName: string): AssetType;
6
+ export declare function stowmetaPath(relativePath: string): string;
7
+ export declare function readStowmeta(srcArtDir: string, relativePath: string): Promise<StowMeta | null>;
8
+ export declare function writeStowmeta(srcArtDir: string, relativePath: string, meta: StowMeta): Promise<void>;
9
+ export declare function stowmetaToAssetSettings(meta: StowMeta): {
10
+ type: AssetType;
11
+ settings: AssetSettings;
12
+ };
13
+ export declare function assetSettingsToStowmeta(asset: ProjectAsset): StowMeta;
14
+ export declare function generateDefaultStowmeta(relativePath: string, type?: AssetType): StowMeta;
@@ -0,0 +1,207 @@
1
+ import { AssetType } from '../core/types.js';
2
+ import { KTX2Quality, TextureResize, DracoQualityPreset, AacQuality, AudioSampleRate } 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, } from './disk-project.js';
4
+ import { readTextFile, writeFile } from '../node-fs.js';
5
+ import { defaultAssetSettings } from './state.js';
6
+ // ─── Detect asset type from file extension ──────────────────────────────────
7
+ const TEXTURE_EXTENSIONS = new Set([
8
+ 'png', 'jpg', 'jpeg', 'bmp', 'tga', 'webp', 'gif',
9
+ ]);
10
+ const AUDIO_EXTENSIONS = new Set([
11
+ 'wav', 'mp3', 'ogg', 'flac', 'aac', 'm4a',
12
+ ]);
13
+ const MESH_EXTENSIONS = new Set([
14
+ 'fbx', 'obj', 'gltf', 'glb',
15
+ ]);
16
+ export function detectAssetType(fileName) {
17
+ const ext = fileName.split('.').pop()?.toLowerCase() ?? '';
18
+ if (TEXTURE_EXTENSIONS.has(ext))
19
+ return AssetType.Texture2D;
20
+ if (AUDIO_EXTENSIONS.has(ext))
21
+ return AssetType.Audio;
22
+ if (MESH_EXTENSIONS.has(ext))
23
+ return AssetType.StaticMesh;
24
+ return AssetType.Unknown;
25
+ }
26
+ // ─── Stowmeta path helper ───────────────────────────────────────────────────
27
+ export function stowmetaPath(relativePath) {
28
+ return `${relativePath}.stowmeta`;
29
+ }
30
+ // ─── Read .stowmeta ─────────────────────────────────────────────────────────
31
+ export async function readStowmeta(srcArtDir, relativePath) {
32
+ const metaPath = stowmetaPath(relativePath);
33
+ const text = await readTextFile(srcArtDir, metaPath);
34
+ if (!text)
35
+ return null;
36
+ try {
37
+ return JSON.parse(text);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ // ─── Write .stowmeta ────────────────────────────────────────────────────────
44
+ export async function writeStowmeta(srcArtDir, relativePath, meta) {
45
+ try {
46
+ const metaPath = stowmetaPath(relativePath);
47
+ const json = JSON.stringify(meta, null, 2);
48
+ await writeFile(srcArtDir, metaPath, json);
49
+ }
50
+ catch (err) {
51
+ console.warn(`[stowmeta] Failed to write meta for ${relativePath}:`, err);
52
+ }
53
+ }
54
+ // ─── Convert StowMeta → AssetSettings + type ────────────────────────────────
55
+ export function stowmetaToAssetSettings(meta) {
56
+ const base = defaultAssetSettings();
57
+ base.tags = meta.tags ?? [];
58
+ base.pack = meta.pack ?? 'default';
59
+ switch (meta.type) {
60
+ case 'texture':
61
+ return {
62
+ type: AssetType.Texture2D,
63
+ settings: {
64
+ ...base,
65
+ quality: KTX2_QUALITY_STRINGS[meta.quality] ?? KTX2Quality.Fastest,
66
+ resize: TEXTURE_RESIZE_STRINGS[meta.resize] ?? TextureResize.Full,
67
+ generateMipmaps: meta.generateMipmaps ?? false,
68
+ },
69
+ };
70
+ case 'audio':
71
+ return {
72
+ type: AssetType.Audio,
73
+ settings: {
74
+ ...base,
75
+ aacQuality: AAC_QUALITY_STRINGS[meta.aacQuality] ?? AacQuality.Medium,
76
+ audioSampleRate: AUDIO_SAMPLE_RATE_STRINGS[meta.sampleRate] ?? AudioSampleRate.Auto,
77
+ },
78
+ };
79
+ case 'staticMesh':
80
+ return {
81
+ type: AssetType.StaticMesh,
82
+ settings: {
83
+ ...base,
84
+ dracoQuality: DRACO_QUALITY_STRINGS[meta.dracoQuality] ?? DracoQualityPreset.Balanced,
85
+ materialOverrides: parseMaterialOverrides(meta.materialOverrides),
86
+ },
87
+ };
88
+ case 'skinnedMesh':
89
+ return {
90
+ type: AssetType.SkinnedMesh,
91
+ settings: {
92
+ ...base,
93
+ materialOverrides: parseMaterialOverrides(meta.materialOverrides),
94
+ },
95
+ };
96
+ case 'animationClip':
97
+ return {
98
+ type: AssetType.AnimationClip,
99
+ settings: {
100
+ ...base,
101
+ targetMeshId: meta.targetMeshId ?? null,
102
+ },
103
+ };
104
+ case 'materialSchema':
105
+ return {
106
+ type: AssetType.MaterialSchema,
107
+ settings: base,
108
+ };
109
+ default:
110
+ return { type: AssetType.Unknown, settings: base };
111
+ }
112
+ }
113
+ function parseMaterialOverrides(raw) {
114
+ if (!raw)
115
+ return {};
116
+ const result = {};
117
+ for (const [key, value] of Object.entries(raw)) {
118
+ const idx = parseInt(key, 10);
119
+ if (!isNaN(idx))
120
+ result[idx] = value;
121
+ }
122
+ return result;
123
+ }
124
+ // ─── Convert AssetSettings → StowMeta ───────────────────────────────────────
125
+ export function assetSettingsToStowmeta(asset) {
126
+ const base = {
127
+ version: 1,
128
+ stringId: asset.stringId,
129
+ tags: asset.settings.tags,
130
+ pack: asset.settings.pack,
131
+ };
132
+ switch (asset.type) {
133
+ case AssetType.Texture2D:
134
+ return {
135
+ ...base,
136
+ type: 'texture',
137
+ quality: KTX2_QUALITY_TO_STRING[asset.settings.quality] ?? 'fastest',
138
+ resize: TEXTURE_RESIZE_TO_STRING[asset.settings.resize] ?? 'full',
139
+ generateMipmaps: asset.settings.generateMipmaps,
140
+ };
141
+ case AssetType.Audio:
142
+ return {
143
+ ...base,
144
+ type: 'audio',
145
+ aacQuality: AAC_QUALITY_TO_STRING[asset.settings.aacQuality] ?? 'medium',
146
+ sampleRate: AUDIO_SAMPLE_RATE_TO_STRING[asset.settings.audioSampleRate] ?? 'auto',
147
+ };
148
+ case AssetType.StaticMesh:
149
+ return {
150
+ ...base,
151
+ type: 'staticMesh',
152
+ dracoQuality: DRACO_QUALITY_TO_STRING[asset.settings.dracoQuality] ?? 'balanced',
153
+ materialOverrides: serializeMaterialOverrides(asset.settings.materialOverrides),
154
+ };
155
+ case AssetType.SkinnedMesh:
156
+ return {
157
+ ...base,
158
+ type: 'skinnedMesh',
159
+ materialOverrides: serializeMaterialOverrides(asset.settings.materialOverrides),
160
+ };
161
+ case AssetType.AnimationClip:
162
+ return {
163
+ ...base,
164
+ type: 'animationClip',
165
+ targetMeshId: asset.settings.targetMeshId ?? null,
166
+ };
167
+ case AssetType.MaterialSchema:
168
+ return { ...base, type: 'materialSchema' };
169
+ default:
170
+ return { ...base, type: 'materialSchema' };
171
+ }
172
+ }
173
+ function serializeMaterialOverrides(overrides) {
174
+ const result = {};
175
+ for (const [key, value] of Object.entries(overrides))
176
+ result[key] = value;
177
+ return result;
178
+ }
179
+ // ─── Generate default .stowmeta ─────────────────────────────────────────────
180
+ export function generateDefaultStowmeta(relativePath, type) {
181
+ const fileName = relativePath.split('/').pop() ?? relativePath;
182
+ const resolvedType = type ?? detectAssetType(fileName);
183
+ const baseName = fileName.replace(/\.[^.]+$/, '');
184
+ const stringId = resolvedType === AssetType.MaterialSchema ? baseName : baseName.toLowerCase();
185
+ const base = {
186
+ version: 1,
187
+ stringId,
188
+ tags: [],
189
+ pack: 'default',
190
+ };
191
+ switch (resolvedType) {
192
+ case AssetType.Texture2D:
193
+ return { ...base, type: 'texture', quality: 'fastest', resize: 'full', generateMipmaps: false };
194
+ case AssetType.Audio:
195
+ return { ...base, type: 'audio', aacQuality: 'medium', sampleRate: 'auto' };
196
+ case AssetType.StaticMesh:
197
+ return { ...base, type: 'staticMesh', dracoQuality: 'balanced', materialOverrides: {} };
198
+ case AssetType.SkinnedMesh:
199
+ return { ...base, type: 'skinnedMesh', materialOverrides: {} };
200
+ case AssetType.AnimationClip:
201
+ return { ...base, type: 'animationClip', targetMeshId: null };
202
+ case AssetType.MaterialSchema:
203
+ return { ...base, type: 'materialSchema' };
204
+ default:
205
+ return { ...base, type: 'materialSchema' };
206
+ }
207
+ }