@series-inc/stowkit-cli 0.6.38 → 0.6.40

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.
@@ -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>;
@@ -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,
@@ -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 mat file exists
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
  }
@@ -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;
@@ -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;
@@ -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;
@@ -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[];
@@ -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/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
  }
@@ -10,6 +10,7 @@ export interface ScanReport {
10
10
  sourceFiles: number;
11
11
  metaFiles: number;
12
12
  matFiles: number;
13
+ spritesheetFiles: number;
13
14
  newFiles: string[];
14
15
  totalAssets: number;
15
16
  }
@@ -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
@@ -8,7 +8,8 @@ import { BlobStore } from './app/blob-store.js';
8
8
  import { readProjectConfig, scanDirectory, readFile, writeFile, renameFile, deleteFile, getFileSnapshot, probeImageDimensions, } from './node-fs.js';
9
9
  import { detectAssetType, readStowmeta, writeStowmeta, stowmetaToAssetSettings, assetSettingsToStowmeta, generateDefaultStowmeta, glbChildToAssetSettings, generateDefaultGlbChild, writeGlbChildSettings, } from './app/stowmeta-io.js';
10
10
  import { readStowmat, writeStowmat, stowmatToMaterialConfig, materialConfigToStowmat } from './app/stowmat-io.js';
11
- import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, } from './app/process-cache.js';
11
+ import { readStowSpriteSheet, writeStowSpriteSheet, stowSpriteSheetToConfig, spritesheetConfigToStowSpriteSheet } from './app/stowspritesheet-io.js';
12
+ import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, deleteCacheFile, } from './app/process-cache.js';
12
13
  import { buildPack, validatePackDependencies, processExtractedAnimations } from './pipeline.js';
13
14
  import { syncRuntimeAssets } from './sync-runtime-assets.js';
14
15
  import { parseGlb, pbrToMaterialConfig } from './encoders/glb-loader.js';
@@ -637,8 +638,16 @@ async function openProject(projectDir) {
637
638
  BlobStore.setProcessed(key, data);
638
639
  }
639
640
  }
640
- asset.status = 'ready';
641
- asset.processedSize = BlobStore.getProcessed(file.relativePath)?.length ?? 0;
641
+ const processedData = BlobStore.getProcessed(file.relativePath);
642
+ if (processedData && processedData.length > 0) {
643
+ asset.status = 'ready';
644
+ asset.processedSize = processedData.length;
645
+ }
646
+ else {
647
+ // Cache corrupt or missing processed blob — delete and reprocess
648
+ console.warn(`[cache] Corrupt cache for ${file.relativePath} — deleting and reprocessing`);
649
+ await deleteCacheFile(projectConfig.srcArtDir, file.relativePath);
650
+ }
642
651
  }
643
652
  }
644
653
  }
@@ -680,6 +689,33 @@ async function openProject(projectDir) {
680
689
  processedSize: 0,
681
690
  });
682
691
  }
692
+ // SpriteSheets from .stowspritesheet files
693
+ for (const ssFile of scan.spritesheetFiles) {
694
+ const id = ssFile.relativePath;
695
+ const sheet = await readStowSpriteSheet(projectConfig.srcArtDir, id);
696
+ const fileName = id.split('/').pop() ?? id;
697
+ const baseName = fileName.replace(/\.[^.]+$/, '');
698
+ let meta = await readStowmeta(projectConfig.srcArtDir, id);
699
+ if (!meta) {
700
+ meta = generateDefaultStowmeta(id, AssetType.SpriteSheet, projectConfig.config.defaults);
701
+ await writeStowmeta(projectConfig.srcArtDir, id, meta);
702
+ }
703
+ const spritesheetConfig = sheet ? stowSpriteSheetToConfig(sheet) : { textureAssetId: null, rows: 1, columns: 1, frameCount: 1, frameRate: 12 };
704
+ const settings = defaultAssetSettings();
705
+ settings.spritesheetConfig = spritesheetConfig;
706
+ settings.pack = meta.pack ?? 'default';
707
+ settings.tags = meta.tags ?? [];
708
+ assets.push({
709
+ id,
710
+ fileName: baseName,
711
+ stringId: meta.stringId || baseName,
712
+ type: AssetType.SpriteSheet,
713
+ status: 'ready',
714
+ settings,
715
+ sourceSize: ssFile.size,
716
+ processedSize: 0,
717
+ });
718
+ }
683
719
  console.log(`[server] Opened: ${projectConfig.projectName} (${assets.length} assets, ${folders.length} folders)`);
684
720
  }
685
721
  // ─── Process single asset ─────────────────────────────────────────────────
@@ -945,6 +981,7 @@ async function handleRequest(req, res, staticApps) {
945
981
  const diskFiles = new Map([
946
982
  ...scan.sourceFiles.map(f => [f.relativePath, f]),
947
983
  ...scan.matFiles.map(f => [f.relativePath, f]),
984
+ ...scan.spritesheetFiles.map(f => [f.relativePath, f]),
948
985
  ]);
949
986
  const diskIds = new Set(diskFiles.keys());
950
987
  // Check for added/removed files
@@ -956,7 +993,7 @@ async function handleRequest(req, res, staticApps) {
956
993
  const modifiedIds = [];
957
994
  if (!changed) {
958
995
  for (const asset of currentAssets) {
959
- if (asset.type === AssetType.MaterialSchema)
996
+ if (asset.type === AssetType.MaterialSchema || asset.type === AssetType.SpriteSheet)
960
997
  continue;
961
998
  const diskFile = diskFiles.get(asset.id);
962
999
  if (diskFile && asset.sourceSize > 0 && diskFile.size !== asset.sourceSize) {
@@ -1146,6 +1183,10 @@ async function handleRequest(req, res, staticApps) {
1146
1183
  const stowmat = materialConfigToStowmat(merged.materialConfig);
1147
1184
  await writeStowmat(projectConfig.srcArtDir, id, stowmat);
1148
1185
  }
1186
+ if (asset.type === AssetType.SpriteSheet && body.settings.spritesheetConfig) {
1187
+ const sheet = spritesheetConfigToStowSpriteSheet(merged.spritesheetConfig);
1188
+ await writeStowSpriteSheet(projectConfig.srcArtDir, id, sheet);
1189
+ }
1149
1190
  }
1150
1191
  }
1151
1192
  // Check if needs reprocessing
@@ -1551,6 +1592,43 @@ async function handleRequest(req, res, staticApps) {
1551
1592
  json(res, { ok: true, asset });
1552
1593
  return;
1553
1594
  }
1595
+ // POST /api/create-spritesheet
1596
+ if (pathname === '/api/create-spritesheet' && req.method === 'POST') {
1597
+ if (!projectConfig) {
1598
+ json(res, { error: 'No project open' }, 400);
1599
+ return;
1600
+ }
1601
+ const body = JSON.parse(await readBody(req));
1602
+ const targetFolder = body.targetFolder ?? '';
1603
+ const name = body.name?.trim() || `SpriteSheet ${assets.filter(a => a.type === AssetType.SpriteSheet).length + 1}`;
1604
+ const baseName = `${name.replace(/\s+/g, '_')}.stowspritesheet`;
1605
+ const fileName = targetFolder ? `${targetFolder}/${baseName}` : baseName;
1606
+ const settings = defaultAssetSettings();
1607
+ settings.spritesheetConfig = {
1608
+ textureAssetId: null,
1609
+ rows: 1,
1610
+ columns: 1,
1611
+ frameCount: 1,
1612
+ frameRate: 12,
1613
+ };
1614
+ const asset = {
1615
+ id: fileName,
1616
+ fileName: name,
1617
+ stringId: name,
1618
+ type: AssetType.SpriteSheet,
1619
+ status: 'ready',
1620
+ settings,
1621
+ sourceSize: 0,
1622
+ processedSize: 0,
1623
+ };
1624
+ assets.push(asset);
1625
+ const stowSpriteSheet = spritesheetConfigToStowSpriteSheet(settings.spritesheetConfig);
1626
+ await writeStowSpriteSheet(projectConfig.srcArtDir, fileName, stowSpriteSheet);
1627
+ const meta = assetSettingsToStowmeta(asset);
1628
+ await writeStowmeta(projectConfig.srcArtDir, fileName, meta);
1629
+ json(res, { ok: true, asset });
1630
+ return;
1631
+ }
1554
1632
  // POST /api/create-folder
1555
1633
  if (pathname === '/api/create-folder' && req.method === 'POST') {
1556
1634
  if (!projectConfig) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/stowkit-cli",
3
- "version": "0.6.38",
3
+ "version": "0.6.40",
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.25",
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",