@series-inc/stowkit-cli 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/blob-store.d.ts +2 -0
- package/dist/app/blob-store.js +6 -0
- package/dist/app/disk-project.d.ts +30 -1
- package/dist/app/process-cache.js +12 -0
- package/dist/app/state.d.ts +4 -0
- package/dist/app/state.js +2 -0
- package/dist/app/stowmeta-io.d.ts +18 -1
- package/dist/app/stowmeta-io.js +214 -3
- package/dist/cleanup.js +2 -0
- package/dist/core/types.d.ts +2 -1
- package/dist/core/types.js +1 -0
- package/dist/encoders/fbx-loader.d.ts +2 -2
- package/dist/encoders/fbx-loader.js +140 -2
- package/dist/encoders/glb-loader.d.ts +42 -0
- package/dist/encoders/glb-loader.js +592 -0
- package/dist/encoders/interfaces.d.ts +4 -1
- package/dist/node-fs.js +2 -0
- package/dist/orchestrator.js +253 -50
- package/dist/pipeline.d.ts +20 -1
- package/dist/pipeline.js +138 -2
- package/dist/server.js +663 -125
- package/dist/workers/process-worker.d.ts +1 -0
- package/dist/workers/process-worker.js +83 -0
- package/dist/workers/worker-pool.d.ts +41 -0
- package/dist/workers/worker-pool.js +130 -0
- package/package.json +2 -2
- package/skill.md +164 -11
package/dist/app/blob-store.d.ts
CHANGED
package/dist/app/blob-store.js
CHANGED
|
@@ -25,7 +25,9 @@ interface StowMetaBase {
|
|
|
25
25
|
stringId: string;
|
|
26
26
|
tags: string[];
|
|
27
27
|
pack?: string;
|
|
28
|
+
excluded?: boolean;
|
|
28
29
|
cache?: StowMetaCache;
|
|
30
|
+
parentId?: string;
|
|
29
31
|
}
|
|
30
32
|
export interface StowMetaTexture extends StowMetaBase {
|
|
31
33
|
type: 'texture';
|
|
@@ -42,6 +44,7 @@ export interface StowMetaStaticMesh extends StowMetaBase {
|
|
|
42
44
|
type: 'staticMesh';
|
|
43
45
|
dracoQuality: string;
|
|
44
46
|
materialOverrides: Record<string, string | null>;
|
|
47
|
+
preserveHierarchy?: boolean;
|
|
45
48
|
}
|
|
46
49
|
export interface StowMetaSkinnedMesh extends StowMetaBase {
|
|
47
50
|
type: 'skinnedMesh';
|
|
@@ -54,7 +57,33 @@ export interface StowMetaAnimationClip extends StowMetaBase {
|
|
|
54
57
|
export interface StowMetaMaterialSchema extends StowMetaBase {
|
|
55
58
|
type: 'materialSchema';
|
|
56
59
|
}
|
|
57
|
-
export
|
|
60
|
+
export interface StowMetaGlbChild {
|
|
61
|
+
name: string;
|
|
62
|
+
childType: string;
|
|
63
|
+
stringId: string;
|
|
64
|
+
excluded?: boolean;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
pack?: string;
|
|
67
|
+
cache?: StowMetaCache;
|
|
68
|
+
quality?: string;
|
|
69
|
+
resize?: string;
|
|
70
|
+
generateMipmaps?: boolean;
|
|
71
|
+
aacQuality?: string;
|
|
72
|
+
sampleRate?: string;
|
|
73
|
+
dracoQuality?: string;
|
|
74
|
+
materialOverrides?: Record<string, string | null>;
|
|
75
|
+
targetMeshId?: string | null;
|
|
76
|
+
materialConfig?: {
|
|
77
|
+
schemaId: string;
|
|
78
|
+
properties: StowMatProperty[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface StowMetaGlbContainer extends StowMetaBase {
|
|
82
|
+
type: 'glbContainer';
|
|
83
|
+
preserveHierarchy?: boolean;
|
|
84
|
+
children: StowMetaGlbChild[];
|
|
85
|
+
}
|
|
86
|
+
export type StowMeta = StowMetaTexture | StowMetaAudio | StowMetaStaticMesh | StowMetaSkinnedMesh | StowMetaAnimationClip | StowMetaMaterialSchema | StowMetaGlbContainer;
|
|
58
87
|
export interface StowMatProperty {
|
|
59
88
|
fieldName: string;
|
|
60
89
|
fieldType: string;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { readFile, writeFile, deleteFile } from '../node-fs.js';
|
|
2
2
|
// ─── Cache File Path ─────────────────────────────────────────────────────────
|
|
3
|
+
const GLB_CONTAINER_EXTS = /\.(glb|gltf)\//i;
|
|
3
4
|
export function cachePath(relativePath) {
|
|
5
|
+
// For container children, redirect to companion dir
|
|
6
|
+
const match = relativePath.match(GLB_CONTAINER_EXTS);
|
|
7
|
+
if (match) {
|
|
8
|
+
const containerEnd = relativePath.indexOf('/', match.index);
|
|
9
|
+
const containerPath = relativePath.substring(0, containerEnd);
|
|
10
|
+
const childName = relativePath.substring(containerEnd + 1);
|
|
11
|
+
return `${containerPath}.children/${childName}.stowcache`;
|
|
12
|
+
}
|
|
4
13
|
return `${relativePath}.stowcache`;
|
|
5
14
|
}
|
|
6
15
|
// ─── Binary Cache Format ─────────────────────────────────────────────────────
|
|
@@ -101,6 +110,9 @@ export function computeSettingsHash(type, settings) {
|
|
|
101
110
|
case 6: // AnimationClip
|
|
102
111
|
parts.push('v2');
|
|
103
112
|
break;
|
|
113
|
+
case 7: // GlbContainer
|
|
114
|
+
parts.push('v1');
|
|
115
|
+
break;
|
|
104
116
|
}
|
|
105
117
|
return parts.join('|');
|
|
106
118
|
}
|
package/dist/app/state.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface AssetSettings {
|
|
|
22
22
|
materialConfig: MaterialConfig;
|
|
23
23
|
materialOverrides: Record<number, string | null>;
|
|
24
24
|
pack: string;
|
|
25
|
+
excluded: boolean;
|
|
26
|
+
preserveHierarchy: boolean;
|
|
25
27
|
}
|
|
26
28
|
export declare function defaultAssetSettings(): AssetSettings;
|
|
27
29
|
export interface ProjectAsset {
|
|
@@ -35,4 +37,6 @@ export interface ProjectAsset {
|
|
|
35
37
|
metadata?: TextureMetadata | AudioMetadata | MeshMetadata | AnimationClipMetadata | SkinnedMeshMetadata;
|
|
36
38
|
sourceSize: number;
|
|
37
39
|
processedSize: number;
|
|
40
|
+
parentId?: string;
|
|
41
|
+
locked?: boolean;
|
|
38
42
|
}
|
package/dist/app/state.js
CHANGED
|
@@ -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 } from './disk-project.js';
|
|
4
|
+
import type { 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>;
|
|
@@ -12,3 +12,20 @@ export declare function stowmetaToAssetSettings(meta: StowMeta): {
|
|
|
12
12
|
};
|
|
13
13
|
export declare function assetSettingsToStowmeta(asset: ProjectAsset): StowMeta;
|
|
14
14
|
export declare function generateDefaultStowmeta(relativePath: string, type?: AssetType): StowMeta;
|
|
15
|
+
/** Generate a default inline child entry for a GLB container. */
|
|
16
|
+
export declare function generateDefaultGlbChild(name: string, childType: string): StowMetaGlbChild;
|
|
17
|
+
/** Convert an inline child entry to { type, settings } for use in ProjectAsset. */
|
|
18
|
+
export declare function glbChildToAssetSettings(child: StowMetaGlbChild): {
|
|
19
|
+
type: AssetType;
|
|
20
|
+
settings: AssetSettings;
|
|
21
|
+
};
|
|
22
|
+
/** Convert runtime AssetSettings back to inline child fields (merges into existing child). */
|
|
23
|
+
export declare function assetSettingsToGlbChild(name: string, childType: string, stringId: string, settings: AssetSettings, existing?: StowMetaGlbChild): StowMetaGlbChild;
|
|
24
|
+
/** Read a specific child's settings from the container's stowmeta. */
|
|
25
|
+
export declare function readGlbChildSettings(srcArtDir: string, containerId: string, childName: string): Promise<{
|
|
26
|
+
child: StowMetaGlbChild;
|
|
27
|
+
type: AssetType;
|
|
28
|
+
settings: AssetSettings;
|
|
29
|
+
} | null>;
|
|
30
|
+
/** Write/merge a child's settings into the container's stowmeta. */
|
|
31
|
+
export declare function writeGlbChildSettings(srcArtDir: string, containerId: string, childName: string, stringId: string, childType: string, settings: AssetSettings): Promise<void>;
|
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 } 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';
|
|
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';
|
|
4
4
|
import { readTextFile, writeFile } from '../node-fs.js';
|
|
5
5
|
import { defaultAssetSettings } from './state.js';
|
|
6
6
|
// ─── Detect asset type from file extension ──────────────────────────────────
|
|
@@ -11,7 +11,10 @@ const AUDIO_EXTENSIONS = new Set([
|
|
|
11
11
|
'wav', 'mp3', 'ogg', 'flac', 'aac', 'm4a',
|
|
12
12
|
]);
|
|
13
13
|
const MESH_EXTENSIONS = new Set([
|
|
14
|
-
'fbx', 'obj',
|
|
14
|
+
'fbx', 'obj',
|
|
15
|
+
]);
|
|
16
|
+
const GLB_EXTENSIONS = new Set([
|
|
17
|
+
'gltf', 'glb',
|
|
15
18
|
]);
|
|
16
19
|
export function detectAssetType(fileName) {
|
|
17
20
|
const ext = fileName.split('.').pop()?.toLowerCase() ?? '';
|
|
@@ -21,6 +24,8 @@ export function detectAssetType(fileName) {
|
|
|
21
24
|
return AssetType.Audio;
|
|
22
25
|
if (MESH_EXTENSIONS.has(ext))
|
|
23
26
|
return AssetType.StaticMesh;
|
|
27
|
+
if (GLB_EXTENSIONS.has(ext))
|
|
28
|
+
return AssetType.GlbContainer;
|
|
24
29
|
return AssetType.Unknown;
|
|
25
30
|
}
|
|
26
31
|
// ─── Stowmeta path helper ───────────────────────────────────────────────────
|
|
@@ -56,6 +61,7 @@ export function stowmetaToAssetSettings(meta) {
|
|
|
56
61
|
const base = defaultAssetSettings();
|
|
57
62
|
base.tags = meta.tags ?? [];
|
|
58
63
|
base.pack = meta.pack ?? 'default';
|
|
64
|
+
base.excluded = meta.excluded ?? false;
|
|
59
65
|
switch (meta.type) {
|
|
60
66
|
case 'texture':
|
|
61
67
|
return {
|
|
@@ -83,6 +89,7 @@ export function stowmetaToAssetSettings(meta) {
|
|
|
83
89
|
...base,
|
|
84
90
|
dracoQuality: DRACO_QUALITY_STRINGS[meta.dracoQuality] ?? DracoQualityPreset.Balanced,
|
|
85
91
|
materialOverrides: parseMaterialOverrides(meta.materialOverrides),
|
|
92
|
+
preserveHierarchy: meta.preserveHierarchy ?? false,
|
|
86
93
|
},
|
|
87
94
|
};
|
|
88
95
|
case 'skinnedMesh':
|
|
@@ -106,6 +113,14 @@ export function stowmetaToAssetSettings(meta) {
|
|
|
106
113
|
type: AssetType.MaterialSchema,
|
|
107
114
|
settings: base,
|
|
108
115
|
};
|
|
116
|
+
case 'glbContainer':
|
|
117
|
+
return {
|
|
118
|
+
type: AssetType.GlbContainer,
|
|
119
|
+
settings: {
|
|
120
|
+
...base,
|
|
121
|
+
preserveHierarchy: meta.preserveHierarchy ?? false,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
109
124
|
default:
|
|
110
125
|
return { type: AssetType.Unknown, settings: base };
|
|
111
126
|
}
|
|
@@ -128,6 +143,7 @@ export function assetSettingsToStowmeta(asset) {
|
|
|
128
143
|
stringId: asset.stringId,
|
|
129
144
|
tags: asset.settings.tags,
|
|
130
145
|
pack: asset.settings.pack,
|
|
146
|
+
excluded: asset.settings.excluded || undefined,
|
|
131
147
|
};
|
|
132
148
|
switch (asset.type) {
|
|
133
149
|
case AssetType.Texture2D:
|
|
@@ -151,6 +167,7 @@ export function assetSettingsToStowmeta(asset) {
|
|
|
151
167
|
type: 'staticMesh',
|
|
152
168
|
dracoQuality: DRACO_QUALITY_TO_STRING[asset.settings.dracoQuality] ?? 'balanced',
|
|
153
169
|
materialOverrides: serializeMaterialOverrides(asset.settings.materialOverrides),
|
|
170
|
+
preserveHierarchy: asset.settings.preserveHierarchy || undefined,
|
|
154
171
|
};
|
|
155
172
|
case AssetType.SkinnedMesh:
|
|
156
173
|
return {
|
|
@@ -166,6 +183,8 @@ export function assetSettingsToStowmeta(asset) {
|
|
|
166
183
|
};
|
|
167
184
|
case AssetType.MaterialSchema:
|
|
168
185
|
return { ...base, type: 'materialSchema' };
|
|
186
|
+
case AssetType.GlbContainer:
|
|
187
|
+
return { ...base, type: 'glbContainer', preserveHierarchy: asset.settings.preserveHierarchy || undefined, children: [] };
|
|
169
188
|
default:
|
|
170
189
|
return { ...base, type: 'materialSchema' };
|
|
171
190
|
}
|
|
@@ -201,7 +220,199 @@ export function generateDefaultStowmeta(relativePath, type) {
|
|
|
201
220
|
return { ...base, type: 'animationClip', targetMeshId: null };
|
|
202
221
|
case AssetType.MaterialSchema:
|
|
203
222
|
return { ...base, type: 'materialSchema' };
|
|
223
|
+
case AssetType.GlbContainer:
|
|
224
|
+
return { ...base, type: 'glbContainer', children: [] };
|
|
204
225
|
default:
|
|
205
226
|
return { ...base, type: 'materialSchema' };
|
|
206
227
|
}
|
|
207
228
|
}
|
|
229
|
+
// ─── GLB Inline Child Helpers ───────────────────────────────────────────────
|
|
230
|
+
/** Generate a default inline child entry for a GLB container. */
|
|
231
|
+
export function generateDefaultGlbChild(name, childType) {
|
|
232
|
+
const baseName = name.replace(/\.[^.]+$/, '');
|
|
233
|
+
const stringId = childType === 'materialSchema' ? baseName : baseName.toLowerCase();
|
|
234
|
+
const child = { name, childType, stringId };
|
|
235
|
+
switch (childType) {
|
|
236
|
+
case 'texture':
|
|
237
|
+
child.quality = 'fastest';
|
|
238
|
+
child.resize = 'full';
|
|
239
|
+
child.generateMipmaps = false;
|
|
240
|
+
break;
|
|
241
|
+
case 'audio':
|
|
242
|
+
child.aacQuality = 'medium';
|
|
243
|
+
child.sampleRate = 'auto';
|
|
244
|
+
break;
|
|
245
|
+
case 'staticMesh':
|
|
246
|
+
case 'skinnedMesh':
|
|
247
|
+
child.dracoQuality = 'balanced';
|
|
248
|
+
child.materialOverrides = {};
|
|
249
|
+
break;
|
|
250
|
+
case 'animationClip':
|
|
251
|
+
child.targetMeshId = null;
|
|
252
|
+
break;
|
|
253
|
+
case 'materialSchema':
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
return child;
|
|
257
|
+
}
|
|
258
|
+
/** Convert an inline child entry to { type, settings } for use in ProjectAsset. */
|
|
259
|
+
export function glbChildToAssetSettings(child) {
|
|
260
|
+
const base = defaultAssetSettings();
|
|
261
|
+
base.tags = child.tags ?? [];
|
|
262
|
+
base.pack = child.pack ?? 'default';
|
|
263
|
+
base.excluded = child.excluded ?? false;
|
|
264
|
+
switch (child.childType) {
|
|
265
|
+
case 'texture':
|
|
266
|
+
return {
|
|
267
|
+
type: AssetType.Texture2D,
|
|
268
|
+
settings: {
|
|
269
|
+
...base,
|
|
270
|
+
quality: KTX2_QUALITY_STRINGS[child.quality ?? 'fastest'] ?? KTX2Quality.Fastest,
|
|
271
|
+
resize: TEXTURE_RESIZE_STRINGS[child.resize ?? 'full'] ?? TextureResize.Full,
|
|
272
|
+
generateMipmaps: child.generateMipmaps ?? false,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
case 'audio':
|
|
276
|
+
return {
|
|
277
|
+
type: AssetType.Audio,
|
|
278
|
+
settings: {
|
|
279
|
+
...base,
|
|
280
|
+
aacQuality: AAC_QUALITY_STRINGS[child.aacQuality ?? 'medium'] ?? AacQuality.Medium,
|
|
281
|
+
audioSampleRate: AUDIO_SAMPLE_RATE_STRINGS[child.sampleRate ?? 'auto'] ?? AudioSampleRate.Auto,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
case 'staticMesh':
|
|
285
|
+
return {
|
|
286
|
+
type: AssetType.StaticMesh,
|
|
287
|
+
settings: {
|
|
288
|
+
...base,
|
|
289
|
+
dracoQuality: DRACO_QUALITY_STRINGS[child.dracoQuality ?? 'balanced'] ?? DracoQualityPreset.Balanced,
|
|
290
|
+
materialOverrides: parseMaterialOverrides(child.materialOverrides),
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
case 'skinnedMesh':
|
|
294
|
+
return {
|
|
295
|
+
type: AssetType.SkinnedMesh,
|
|
296
|
+
settings: {
|
|
297
|
+
...base,
|
|
298
|
+
materialOverrides: parseMaterialOverrides(child.materialOverrides),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
case 'animationClip':
|
|
302
|
+
return {
|
|
303
|
+
type: AssetType.AnimationClip,
|
|
304
|
+
settings: {
|
|
305
|
+
...base,
|
|
306
|
+
targetMeshId: child.targetMeshId ?? null,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
case 'materialSchema':
|
|
310
|
+
if (child.materialConfig) {
|
|
311
|
+
base.materialConfig = glbChildMaterialConfigToRuntime(child.materialConfig);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
type: AssetType.MaterialSchema,
|
|
315
|
+
settings: base,
|
|
316
|
+
};
|
|
317
|
+
default:
|
|
318
|
+
return { type: AssetType.Unknown, settings: base };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Convert runtime AssetSettings back to inline child fields (merges into existing child). */
|
|
322
|
+
export function assetSettingsToGlbChild(name, childType, stringId, settings, existing) {
|
|
323
|
+
const child = {
|
|
324
|
+
...(existing ?? {}),
|
|
325
|
+
name,
|
|
326
|
+
childType,
|
|
327
|
+
stringId,
|
|
328
|
+
};
|
|
329
|
+
if (settings.tags?.length)
|
|
330
|
+
child.tags = settings.tags;
|
|
331
|
+
else
|
|
332
|
+
delete child.tags;
|
|
333
|
+
if (settings.pack && settings.pack !== 'default')
|
|
334
|
+
child.pack = settings.pack;
|
|
335
|
+
else
|
|
336
|
+
delete child.pack;
|
|
337
|
+
if (settings.excluded)
|
|
338
|
+
child.excluded = true;
|
|
339
|
+
else
|
|
340
|
+
delete child.excluded;
|
|
341
|
+
switch (childType) {
|
|
342
|
+
case 'texture':
|
|
343
|
+
child.quality = KTX2_QUALITY_TO_STRING[settings.quality] ?? 'fastest';
|
|
344
|
+
child.resize = TEXTURE_RESIZE_TO_STRING[settings.resize] ?? 'full';
|
|
345
|
+
child.generateMipmaps = settings.generateMipmaps;
|
|
346
|
+
break;
|
|
347
|
+
case 'audio':
|
|
348
|
+
child.aacQuality = AAC_QUALITY_TO_STRING[settings.aacQuality] ?? 'medium';
|
|
349
|
+
child.sampleRate = AUDIO_SAMPLE_RATE_TO_STRING[settings.audioSampleRate] ?? 'auto';
|
|
350
|
+
break;
|
|
351
|
+
case 'staticMesh':
|
|
352
|
+
case 'skinnedMesh':
|
|
353
|
+
child.dracoQuality = DRACO_QUALITY_TO_STRING[settings.dracoQuality] ?? 'balanced';
|
|
354
|
+
child.materialOverrides = serializeMaterialOverrides(settings.materialOverrides);
|
|
355
|
+
break;
|
|
356
|
+
case 'animationClip':
|
|
357
|
+
child.targetMeshId = settings.targetMeshId ?? null;
|
|
358
|
+
break;
|
|
359
|
+
case 'materialSchema':
|
|
360
|
+
if (settings.materialConfig && settings.materialConfig.properties.length > 0) {
|
|
361
|
+
child.materialConfig = runtimeMaterialConfigToGlbChild(settings.materialConfig);
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
return child;
|
|
366
|
+
}
|
|
367
|
+
/** Read a specific child's settings from the container's stowmeta. */
|
|
368
|
+
export async function readGlbChildSettings(srcArtDir, containerId, childName) {
|
|
369
|
+
const containerMeta = await readStowmeta(srcArtDir, containerId);
|
|
370
|
+
if (!containerMeta || containerMeta.type !== 'glbContainer')
|
|
371
|
+
return null;
|
|
372
|
+
const child = containerMeta.children.find(c => c.name === childName);
|
|
373
|
+
if (!child)
|
|
374
|
+
return null;
|
|
375
|
+
const { type, settings } = glbChildToAssetSettings(child);
|
|
376
|
+
return { child, type, settings };
|
|
377
|
+
}
|
|
378
|
+
/** Write/merge a child's settings into the container's stowmeta. */
|
|
379
|
+
export async function writeGlbChildSettings(srcArtDir, containerId, childName, stringId, childType, settings) {
|
|
380
|
+
const containerMeta = await readStowmeta(srcArtDir, containerId);
|
|
381
|
+
if (!containerMeta || containerMeta.type !== 'glbContainer')
|
|
382
|
+
return;
|
|
383
|
+
const idx = containerMeta.children.findIndex(c => c.name === childName);
|
|
384
|
+
const existing = idx >= 0 ? containerMeta.children[idx] : undefined;
|
|
385
|
+
const updated = assetSettingsToGlbChild(childName, childType, stringId, settings, existing);
|
|
386
|
+
if (idx >= 0) {
|
|
387
|
+
containerMeta.children[idx] = updated;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
containerMeta.children.push(updated);
|
|
391
|
+
}
|
|
392
|
+
await writeStowmeta(srcArtDir, containerId, containerMeta);
|
|
393
|
+
}
|
|
394
|
+
// ─── Material config conversion for inline GLB children ─────────────────────
|
|
395
|
+
function glbChildMaterialConfigToRuntime(config) {
|
|
396
|
+
return {
|
|
397
|
+
schemaId: config.schemaId ?? '',
|
|
398
|
+
properties: (config.properties ?? []).map((prop) => ({
|
|
399
|
+
fieldName: prop.fieldName ?? '',
|
|
400
|
+
fieldType: MATERIAL_FIELD_TYPE_STRINGS[prop.fieldType] ?? MaterialFieldType.Color,
|
|
401
|
+
previewFlag: PREVIEW_FLAG_STRINGS[prop.previewFlag] ?? PreviewPropertyFlag.None,
|
|
402
|
+
value: prop.value ?? [1, 1, 1, 1],
|
|
403
|
+
textureAssetId: prop.textureAsset ?? null,
|
|
404
|
+
})),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function runtimeMaterialConfigToGlbChild(config) {
|
|
408
|
+
return {
|
|
409
|
+
schemaId: config.schemaId,
|
|
410
|
+
properties: config.properties.map((prop) => ({
|
|
411
|
+
fieldName: prop.fieldName,
|
|
412
|
+
fieldType: MATERIAL_FIELD_TYPE_TO_STRING[prop.fieldType] ?? 'color',
|
|
413
|
+
previewFlag: PREVIEW_FLAG_TO_STRING[prop.previewFlag] ?? 'none',
|
|
414
|
+
value: [...prop.value],
|
|
415
|
+
textureAsset: prop.textureAssetId,
|
|
416
|
+
})),
|
|
417
|
+
};
|
|
418
|
+
}
|
package/dist/cleanup.js
CHANGED
|
@@ -48,6 +48,8 @@ async function walkForOrphans(basePath, prefix, sourceFiles, matFiles, onOrphan)
|
|
|
48
48
|
if (entry.isDirectory()) {
|
|
49
49
|
if (entry.name.startsWith('.'))
|
|
50
50
|
continue;
|
|
51
|
+
if (entry.name.endsWith('.children'))
|
|
52
|
+
continue;
|
|
51
53
|
await walkForOrphans(basePath, relativePath, sourceFiles, matFiles, onOrphan);
|
|
52
54
|
}
|
|
53
55
|
else if (entry.isFile()) {
|
package/dist/core/types.d.ts
CHANGED
package/dist/core/types.js
CHANGED
|
@@ -8,6 +8,7 @@ export var AssetType;
|
|
|
8
8
|
AssetType[AssetType["MaterialSchema"] = 4] = "MaterialSchema";
|
|
9
9
|
AssetType[AssetType["SkinnedMesh"] = 5] = "SkinnedMesh";
|
|
10
10
|
AssetType[AssetType["AnimationClip"] = 6] = "AnimationClip";
|
|
11
|
+
AssetType[AssetType["GlbContainer"] = 7] = "GlbContainer";
|
|
11
12
|
})(AssetType || (AssetType = {}));
|
|
12
13
|
// ─── Texture Enums ──────────────────────────────────────────────────────────
|
|
13
14
|
export var KTX2Quality;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IMeshImporter, ImportedMesh } from './interfaces.js';
|
|
1
|
+
import type { IMeshImporter, MeshImportOpts, ImportedMesh } from './interfaces.js';
|
|
2
2
|
export declare class NodeFbxImporter implements IMeshImporter {
|
|
3
|
-
import(data: Uint8Array, _fileName: string): Promise<ImportedMesh>;
|
|
3
|
+
import(data: Uint8Array, _fileName: string, opts?: MeshImportOpts): Promise<ImportedMesh>;
|
|
4
4
|
}
|
|
@@ -49,13 +49,15 @@ function ensureDomShim() {
|
|
|
49
49
|
}
|
|
50
50
|
// ─── FBX Importer ────────────────────────────────────────────────────────────
|
|
51
51
|
export class NodeFbxImporter {
|
|
52
|
-
async import(data, _fileName) {
|
|
52
|
+
async import(data, _fileName, opts) {
|
|
53
53
|
ensureDomShim();
|
|
54
54
|
try {
|
|
55
55
|
const loader = new FBXLoader();
|
|
56
56
|
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
57
57
|
const group = loader.parse(buffer, '');
|
|
58
|
-
const mesh =
|
|
58
|
+
const mesh = opts?.preserveHierarchy
|
|
59
|
+
? extractMeshFromGroupHierarchical(group)
|
|
60
|
+
: extractMeshFromGroup(group);
|
|
59
61
|
mesh.animations = parseFbxAnimations(data);
|
|
60
62
|
return mesh;
|
|
61
63
|
}
|
|
@@ -298,6 +300,142 @@ function extractMeshFromGroup(group) {
|
|
|
298
300
|
animations: [],
|
|
299
301
|
};
|
|
300
302
|
}
|
|
303
|
+
// ─── Hierarchical Group → ImportedMesh extraction ────────────────────────────
|
|
304
|
+
function extractMeshFromGroupHierarchical(group) {
|
|
305
|
+
group.updateMatrixWorld(true);
|
|
306
|
+
// Collect all relevant nodes — skip Bone, Light, Camera, SkinnedMesh
|
|
307
|
+
const nodeList = [];
|
|
308
|
+
const nodeIndexMap = new Map();
|
|
309
|
+
function collectNodes(obj) {
|
|
310
|
+
if (obj.isBone)
|
|
311
|
+
return;
|
|
312
|
+
if (obj.isLight)
|
|
313
|
+
return;
|
|
314
|
+
if (obj.isCamera)
|
|
315
|
+
return;
|
|
316
|
+
if (obj.isSkinnedMesh)
|
|
317
|
+
return;
|
|
318
|
+
nodeIndexMap.set(obj, nodeList.length);
|
|
319
|
+
nodeList.push(obj);
|
|
320
|
+
for (const child of obj.children) {
|
|
321
|
+
collectNodes(child);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const child of group.children) {
|
|
325
|
+
collectNodes(child);
|
|
326
|
+
}
|
|
327
|
+
if (nodeList.length === 0)
|
|
328
|
+
return emptyMesh();
|
|
329
|
+
const subMeshes = [];
|
|
330
|
+
const materials = [];
|
|
331
|
+
const materialSet = new Map();
|
|
332
|
+
const nodes = [];
|
|
333
|
+
for (let ni = 0; ni < nodeList.length; ni++) {
|
|
334
|
+
const obj = nodeList[ni];
|
|
335
|
+
const isMesh = obj.isMesh;
|
|
336
|
+
// Determine parent index
|
|
337
|
+
let parentIndex = -1;
|
|
338
|
+
let ancestor = obj.parent;
|
|
339
|
+
while (ancestor && ancestor !== group) {
|
|
340
|
+
const idx = nodeIndexMap.get(ancestor);
|
|
341
|
+
if (idx !== undefined) {
|
|
342
|
+
parentIndex = idx;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
ancestor = ancestor.parent;
|
|
346
|
+
}
|
|
347
|
+
// Local transform
|
|
348
|
+
const pos = [obj.position.x, obj.position.y, obj.position.z];
|
|
349
|
+
const rot = [obj.quaternion.x, obj.quaternion.y, obj.quaternion.z, obj.quaternion.w];
|
|
350
|
+
const scl = [obj.scale.x, obj.scale.y, obj.scale.z];
|
|
351
|
+
const meshIndices = [];
|
|
352
|
+
if (isMesh) {
|
|
353
|
+
const mesh = obj;
|
|
354
|
+
const geometry = mesh.geometry;
|
|
355
|
+
const posAttr = geometry.getAttribute('position');
|
|
356
|
+
if (!posAttr) {
|
|
357
|
+
nodes.push({ name: obj.name || `node_${ni}`, parentIndex, position: pos, rotation: rot, scale: scl, meshIndices: [] });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const vertCount = posAttr.count;
|
|
361
|
+
// Store positions in LOCAL space (no matrixWorld bake)
|
|
362
|
+
const positions = new Float32Array(vertCount * 3);
|
|
363
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
364
|
+
positions[vi * 3] = posAttr.getX(vi);
|
|
365
|
+
positions[vi * 3 + 1] = posAttr.getY(vi);
|
|
366
|
+
positions[vi * 3 + 2] = posAttr.getZ(vi);
|
|
367
|
+
}
|
|
368
|
+
// Normals in local space
|
|
369
|
+
let normals = null;
|
|
370
|
+
const normAttr = geometry.getAttribute('normal');
|
|
371
|
+
if (normAttr && normAttr.count === vertCount) {
|
|
372
|
+
normals = new Float32Array(vertCount * 3);
|
|
373
|
+
const localNormalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrix);
|
|
374
|
+
const n = new THREE.Vector3();
|
|
375
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
376
|
+
n.fromBufferAttribute(normAttr, vi);
|
|
377
|
+
n.applyMatrix3(localNormalMatrix).normalize();
|
|
378
|
+
normals[vi * 3] = n.x;
|
|
379
|
+
normals[vi * 3 + 1] = n.y;
|
|
380
|
+
normals[vi * 3 + 2] = n.z;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// UVs — FBX convention: no V-flip
|
|
384
|
+
let uvs = null;
|
|
385
|
+
const uvAttr = geometry.getAttribute('uv');
|
|
386
|
+
if (uvAttr && uvAttr.count === vertCount) {
|
|
387
|
+
uvs = new Float32Array(vertCount * 2);
|
|
388
|
+
for (let vi = 0; vi < vertCount; vi++) {
|
|
389
|
+
uvs[vi * 2] = uvAttr.getX(vi);
|
|
390
|
+
uvs[vi * 2 + 1] = uvAttr.getY(vi);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const indexAttr = geometry.getIndex();
|
|
394
|
+
let indices;
|
|
395
|
+
if (indexAttr) {
|
|
396
|
+
indices = new Uint32Array(indexAttr.count);
|
|
397
|
+
for (let ii = 0; ii < indexAttr.count; ii++)
|
|
398
|
+
indices[ii] = indexAttr.getX(ii);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
indices = new Uint32Array(vertCount);
|
|
402
|
+
for (let ii = 0; ii < vertCount; ii++)
|
|
403
|
+
indices[ii] = ii;
|
|
404
|
+
}
|
|
405
|
+
const meshMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
406
|
+
const matName = meshMaterial?.name || 'default';
|
|
407
|
+
let matIndex;
|
|
408
|
+
if (materialSet.has(matName)) {
|
|
409
|
+
matIndex = materialSet.get(matName);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
matIndex = materials.length;
|
|
413
|
+
materialSet.set(matName, matIndex);
|
|
414
|
+
materials.push({ name: matName });
|
|
415
|
+
}
|
|
416
|
+
meshIndices.push(subMeshes.length);
|
|
417
|
+
subMeshes.push({ positions, normals, uvs, indices, materialIndex: matIndex });
|
|
418
|
+
}
|
|
419
|
+
nodes.push({
|
|
420
|
+
name: obj.name || `node_${ni}`,
|
|
421
|
+
parentIndex,
|
|
422
|
+
position: pos,
|
|
423
|
+
rotation: rot,
|
|
424
|
+
scale: scl,
|
|
425
|
+
meshIndices,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (materials.length === 0)
|
|
429
|
+
materials.push({ name: 'default' });
|
|
430
|
+
return {
|
|
431
|
+
subMeshes,
|
|
432
|
+
materials,
|
|
433
|
+
nodes,
|
|
434
|
+
hasSkeleton: false,
|
|
435
|
+
bones: [],
|
|
436
|
+
animations: [],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
301
439
|
function emptyMesh() {
|
|
302
440
|
return { subMeshes: [], materials: [{ name: 'default' }], nodes: [], hasSkeleton: false, bones: [], animations: [] };
|
|
303
441
|
}
|