@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.
- package/dist/app/blob-store.d.ts +9 -0
- package/dist/app/blob-store.js +42 -0
- package/dist/app/disk-project.d.ts +84 -0
- package/dist/app/disk-project.js +70 -0
- package/dist/app/process-cache.d.ts +10 -0
- package/dist/app/process-cache.js +126 -0
- package/dist/app/state.d.ts +38 -0
- package/dist/app/state.js +16 -0
- package/dist/app/stowmat-io.d.ts +6 -0
- package/dist/app/stowmat-io.js +48 -0
- package/dist/app/stowmeta-io.d.ts +14 -0
- package/dist/app/stowmeta-io.js +207 -0
- package/dist/cleanup.d.ts +3 -0
- package/dist/cleanup.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +148 -0
- package/dist/core/binary.d.ts +41 -0
- package/dist/core/binary.js +118 -0
- package/dist/core/constants.d.ts +64 -0
- package/dist/core/constants.js +65 -0
- package/dist/core/path.d.ts +3 -0
- package/dist/core/path.js +27 -0
- package/dist/core/types.d.ts +204 -0
- package/dist/core/types.js +76 -0
- package/dist/encoders/aac-encoder.d.ts +12 -0
- package/dist/encoders/aac-encoder.js +179 -0
- package/dist/encoders/basis-encoder.d.ts +15 -0
- package/dist/encoders/basis-encoder.js +116 -0
- package/dist/encoders/draco-encoder.d.ts +11 -0
- package/dist/encoders/draco-encoder.js +155 -0
- package/dist/encoders/fbx-loader.d.ts +4 -0
- package/dist/encoders/fbx-loader.js +540 -0
- package/dist/encoders/image-decoder.d.ts +13 -0
- package/dist/encoders/image-decoder.js +33 -0
- package/dist/encoders/interfaces.d.ts +105 -0
- package/dist/encoders/interfaces.js +1 -0
- package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
- package/dist/encoders/skinned-mesh-builder.js +135 -0
- package/dist/format/metadata.d.ts +18 -0
- package/dist/format/metadata.js +381 -0
- package/dist/format/packer.d.ts +8 -0
- package/dist/format/packer.js +87 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +35 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +73 -0
- package/dist/node-fs.d.ts +22 -0
- package/dist/node-fs.js +148 -0
- package/dist/orchestrator.d.ts +20 -0
- package/dist/orchestrator.js +301 -0
- package/dist/pipeline.d.ts +23 -0
- package/dist/pipeline.js +354 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +859 -0
- package/package.json +35 -0
- package/skill.md +211 -0
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { AssetType, TextureChannelFormat, TextureResize, AudioSampleRate } from './core/types.js';
|
|
2
|
+
import { dracoPresetToSettings } from './encoders/draco-encoder.js';
|
|
3
|
+
import { buildSkinnedMeshData } from './encoders/skinned-mesh-builder.js';
|
|
4
|
+
import { serializeTextureMetadata, serializeAudioMetadata, serializeMeshMetadata, serializeAnimationClipMetadata, serializeSkinnedMeshMetadata, serializeMaterialSchemaMetadata, deserializeSkinnedMeshMetadata, deserializeAnimationClipMetadata, } from './format/metadata.js';
|
|
5
|
+
import { StowPacker } from './format/packer.js';
|
|
6
|
+
import { BlobStore } from './app/blob-store.js';
|
|
7
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
8
|
+
function resizeDivisor(resize) {
|
|
9
|
+
switch (resize) {
|
|
10
|
+
case TextureResize.Full: return 1;
|
|
11
|
+
case TextureResize.Half: return 2;
|
|
12
|
+
case TextureResize.Quarter: return 4;
|
|
13
|
+
case TextureResize.Eighth: return 8;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function flipPixelsY(pixels, width, height, channels) {
|
|
17
|
+
const rowBytes = width * channels;
|
|
18
|
+
const temp = new Uint8Array(rowBytes);
|
|
19
|
+
for (let y = 0; y < Math.floor(height / 2); y++) {
|
|
20
|
+
const topOffset = y * rowBytes;
|
|
21
|
+
const botOffset = (height - 1 - y) * rowBytes;
|
|
22
|
+
temp.set(pixels.subarray(topOffset, topOffset + rowBytes));
|
|
23
|
+
pixels.copyWithin(topOffset, botOffset, botOffset + rowBytes);
|
|
24
|
+
pixels.set(temp, botOffset);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function resampleChannel(src, srcRate, dstRate) {
|
|
28
|
+
if (dstRate >= srcRate)
|
|
29
|
+
return src;
|
|
30
|
+
const ratio = srcRate / dstRate;
|
|
31
|
+
const dstLen = Math.ceil(src.length / ratio);
|
|
32
|
+
const dst = new Float32Array(dstLen);
|
|
33
|
+
for (let i = 0; i < dstLen; i++) {
|
|
34
|
+
const srcIdx = i * ratio;
|
|
35
|
+
const lo = Math.floor(srcIdx);
|
|
36
|
+
const hi = Math.min(lo + 1, src.length - 1);
|
|
37
|
+
const frac = srcIdx - lo;
|
|
38
|
+
dst[i] = src[lo] + frac * (src[hi] - src[lo]);
|
|
39
|
+
}
|
|
40
|
+
return dst;
|
|
41
|
+
}
|
|
42
|
+
export async function processAsset(id, sourceData, type, stringId, settings, ctx) {
|
|
43
|
+
const log = (msg) => ctx.onProgress?.(id, msg);
|
|
44
|
+
if (type === AssetType.Texture2D) {
|
|
45
|
+
log('decoding image...');
|
|
46
|
+
let { pixels, width, height } = await ctx.imageDecoder.loadPixels(sourceData);
|
|
47
|
+
const divisor = resizeDivisor(settings.resize);
|
|
48
|
+
if (divisor > 1) {
|
|
49
|
+
({ pixels, width, height } = await ctx.imageDecoder.resizePixels(pixels, width, height, divisor));
|
|
50
|
+
log(`resized to ${width}x${height}`);
|
|
51
|
+
}
|
|
52
|
+
flipPixelsY(pixels, width, height, 4);
|
|
53
|
+
log('encoding KTX2...');
|
|
54
|
+
const result = await ctx.textureEncoder.encode(pixels, width, height, 4, settings.quality, TextureChannelFormat.RGBA, settings.generateMipmaps);
|
|
55
|
+
result.metadata.stringId = stringId;
|
|
56
|
+
BlobStore.setProcessed(id, result.data);
|
|
57
|
+
log(`KTX2 encoded (${(result.data.length / 1024).toFixed(0)} KB)`);
|
|
58
|
+
return { metadata: result.metadata, processedSize: result.data.length };
|
|
59
|
+
}
|
|
60
|
+
if (type === AssetType.StaticMesh) {
|
|
61
|
+
log('importing FBX...');
|
|
62
|
+
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
63
|
+
const verts = imported.subMeshes.reduce((s, m) => s + m.positions.length / 3, 0);
|
|
64
|
+
log(`imported (${verts} verts, ${imported.subMeshes.length} submeshes)`);
|
|
65
|
+
const meshSettings = dracoPresetToSettings(settings.dracoQuality);
|
|
66
|
+
const result = await ctx.meshEncoder.encode(imported, meshSettings);
|
|
67
|
+
result.metadata.stringId = stringId;
|
|
68
|
+
BlobStore.setProcessed(id, result.data);
|
|
69
|
+
log(`Draco encoded (${(result.data.length / 1024).toFixed(0)} KB)`);
|
|
70
|
+
return { metadata: result.metadata, processedSize: result.data.length };
|
|
71
|
+
}
|
|
72
|
+
if (type === AssetType.SkinnedMesh) {
|
|
73
|
+
log('importing FBX...');
|
|
74
|
+
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
75
|
+
const animCount = imported.animations?.length ?? 0;
|
|
76
|
+
log(`imported (${imported.bones?.length ?? 0} bones, ${animCount} clips)`);
|
|
77
|
+
const skinnedResult = buildSkinnedMeshData(imported, 0.01);
|
|
78
|
+
skinnedResult.metadata.stringId = stringId;
|
|
79
|
+
BlobStore.setProcessed(id, skinnedResult.data);
|
|
80
|
+
BlobStore.setProcessed(`${id}:skinnedMeta`, serializeSkinnedMeshMetadata(skinnedResult.metadata));
|
|
81
|
+
if (animCount > 0 && imported.animations) {
|
|
82
|
+
const animBlobs = buildAnimationDataBlobsV2(imported);
|
|
83
|
+
for (let ci = 0; ci < animBlobs.length; ci++) {
|
|
84
|
+
const ab = animBlobs[ci];
|
|
85
|
+
ab.metadata.stringId = animBlobs.length === 1
|
|
86
|
+
? `${stringId}_anim`
|
|
87
|
+
: `${stringId}_anim_${ci}`;
|
|
88
|
+
ab.metadata.targetMeshId = stringId;
|
|
89
|
+
BlobStore.setProcessed(`${id}:anim:${ci}`, ab.data);
|
|
90
|
+
BlobStore.setProcessed(`${id}:animMeta:${ci}`, serializeAnimationClipMetadata(ab.metadata));
|
|
91
|
+
}
|
|
92
|
+
BlobStore.setProcessed(`${id}:animCount`, new Uint8Array([animBlobs.length]));
|
|
93
|
+
}
|
|
94
|
+
log(`skinned mesh built (${(skinnedResult.data.length / 1024).toFixed(0)} KB)`);
|
|
95
|
+
return { metadata: skinnedResult.metadata, processedSize: skinnedResult.data.length };
|
|
96
|
+
}
|
|
97
|
+
if (type === AssetType.AnimationClip) {
|
|
98
|
+
log('importing FBX...');
|
|
99
|
+
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
100
|
+
const animCount = imported.animations?.length ?? 0;
|
|
101
|
+
if (animCount === 0)
|
|
102
|
+
throw new Error('No animation clips found in FBX file');
|
|
103
|
+
const animBlobs = buildAnimationDataBlobsV2(imported);
|
|
104
|
+
if (animBlobs.length === 0)
|
|
105
|
+
throw new Error('All animation tracks were stripped');
|
|
106
|
+
const primary = animBlobs[0];
|
|
107
|
+
primary.metadata.stringId = stringId;
|
|
108
|
+
BlobStore.setProcessed(id, primary.data);
|
|
109
|
+
BlobStore.setProcessed(`${id}:animMeta`, serializeAnimationClipMetadata(primary.metadata));
|
|
110
|
+
for (let ci = 1; ci < animBlobs.length; ci++) {
|
|
111
|
+
const ab = animBlobs[ci];
|
|
112
|
+
ab.metadata.stringId = `${stringId}_${ci}`;
|
|
113
|
+
BlobStore.setProcessed(`${id}:anim:${ci}`, ab.data);
|
|
114
|
+
BlobStore.setProcessed(`${id}:animMeta:${ci}`, serializeAnimationClipMetadata(ab.metadata));
|
|
115
|
+
}
|
|
116
|
+
BlobStore.setProcessed(`${id}:animCount`, new Uint8Array([animBlobs.length]));
|
|
117
|
+
const totalAnimBytes = animBlobs.reduce((s, a) => s + a.data.length, 0);
|
|
118
|
+
log(`animation serialized (${(totalAnimBytes / 1024).toFixed(0)} KB, ${animBlobs.length} clips)`);
|
|
119
|
+
return { metadata: primary.metadata, processedSize: totalAnimBytes };
|
|
120
|
+
}
|
|
121
|
+
if (type === AssetType.Audio) {
|
|
122
|
+
log('decoding audio...');
|
|
123
|
+
const pcm = await ctx.audioDecoder.decodeToPcm(sourceData, id);
|
|
124
|
+
log(`decoded (${pcm.sampleRate}Hz, ${pcm.channels.length}ch, ${pcm.durationMs}ms)`);
|
|
125
|
+
let { channels, sampleRate } = pcm;
|
|
126
|
+
const targetRate = settings.audioSampleRate;
|
|
127
|
+
if (targetRate !== AudioSampleRate.Auto && targetRate < sampleRate) {
|
|
128
|
+
channels = channels.map((ch) => resampleChannel(ch, sampleRate, targetRate));
|
|
129
|
+
sampleRate = targetRate;
|
|
130
|
+
log(`resampled to ${sampleRate}Hz`);
|
|
131
|
+
}
|
|
132
|
+
log('encoding AAC...');
|
|
133
|
+
const aacData = await ctx.aacEncoder.encode(channels, sampleRate, settings.aacQuality);
|
|
134
|
+
BlobStore.setProcessed(id, aacData);
|
|
135
|
+
const metadata = {
|
|
136
|
+
stringId,
|
|
137
|
+
sampleRate,
|
|
138
|
+
channels: pcm.channels.length,
|
|
139
|
+
durationMs: pcm.durationMs,
|
|
140
|
+
};
|
|
141
|
+
log(`AAC encoded (${(aacData.length / 1024).toFixed(0)} KB)`);
|
|
142
|
+
return { metadata, processedSize: aacData.length };
|
|
143
|
+
}
|
|
144
|
+
return { processedSize: 0 };
|
|
145
|
+
}
|
|
146
|
+
// ─── Build Pack ──────────────────────────────────────────────────────────────
|
|
147
|
+
export function buildPack(assets, assetsById) {
|
|
148
|
+
const packer = new StowPacker();
|
|
149
|
+
const materialsByStringId = new Map(assets
|
|
150
|
+
.filter(a => a.type === AssetType.MaterialSchema)
|
|
151
|
+
.map(a => [a.stringId.toLowerCase(), a]));
|
|
152
|
+
for (const asset of assets) {
|
|
153
|
+
if (asset.status !== 'ready')
|
|
154
|
+
continue;
|
|
155
|
+
if (asset.type === AssetType.MaterialSchema) {
|
|
156
|
+
const schemaMeta = buildMaterialSchemaMetadata(asset, assetsById);
|
|
157
|
+
const schemaBytes = serializeMaterialSchemaMetadata(schemaMeta);
|
|
158
|
+
packer.addAsset(asset.stringId, AssetType.MaterialSchema, new Uint8Array(0), schemaBytes, asset.settings.tags);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const processedData = BlobStore.getProcessed(asset.id);
|
|
162
|
+
if (!processedData) {
|
|
163
|
+
console.warn(`[buildPack] Skipping ${asset.id} (type=${asset.type}): no processed data in BlobStore`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
let metadataBytes = null;
|
|
167
|
+
if (asset.type === AssetType.Texture2D && asset.metadata) {
|
|
168
|
+
metadataBytes = serializeTextureMetadata(asset.metadata);
|
|
169
|
+
}
|
|
170
|
+
else if (asset.type === AssetType.Audio && asset.metadata) {
|
|
171
|
+
metadataBytes = serializeAudioMetadata(asset.metadata);
|
|
172
|
+
}
|
|
173
|
+
else if (asset.type === AssetType.StaticMesh && asset.metadata) {
|
|
174
|
+
const meshMeta = applyMaterialAssignments(asset, asset.metadata, assetsById, materialsByStringId);
|
|
175
|
+
meshMeta.stringId = asset.stringId;
|
|
176
|
+
metadataBytes = serializeMeshMetadata(meshMeta);
|
|
177
|
+
}
|
|
178
|
+
else if (asset.type === AssetType.SkinnedMesh) {
|
|
179
|
+
const skinnedMetaBlob = BlobStore.getProcessed(`${asset.id}:skinnedMeta`);
|
|
180
|
+
if (skinnedMetaBlob) {
|
|
181
|
+
const skinnedMeta = deserializeSkinnedMeshMetadata(skinnedMetaBlob);
|
|
182
|
+
const patched = applyMaterialAssignments(asset, skinnedMeta, assetsById, materialsByStringId);
|
|
183
|
+
patched.stringId = asset.stringId;
|
|
184
|
+
metadataBytes = serializeSkinnedMeshMetadata(patched);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
metadataBytes = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (asset.type === AssetType.AnimationClip) {
|
|
191
|
+
const animMetaBlob = BlobStore.getProcessed(`${asset.id}:animMeta`);
|
|
192
|
+
if (animMetaBlob) {
|
|
193
|
+
const animMeta = deserializeAnimationClipMetadata(animMetaBlob);
|
|
194
|
+
animMeta.stringId = asset.stringId;
|
|
195
|
+
if (asset.settings.targetMeshId) {
|
|
196
|
+
const targetMesh = assetsById.get(asset.settings.targetMeshId);
|
|
197
|
+
animMeta.targetMeshId = targetMesh?.type === AssetType.SkinnedMesh ? targetMesh.stringId : '';
|
|
198
|
+
}
|
|
199
|
+
metadataBytes = serializeAnimationClipMetadata(animMeta);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
packer.addAsset(asset.stringId, asset.type, processedData, metadataBytes, asset.settings.tags);
|
|
203
|
+
// Embedded animations for skinned meshes
|
|
204
|
+
if (asset.type === AssetType.SkinnedMesh) {
|
|
205
|
+
const animCountBlob = BlobStore.getProcessed(`${asset.id}:animCount`);
|
|
206
|
+
const clipCount = animCountBlob ? animCountBlob[0] : 0;
|
|
207
|
+
for (let ci = 0; ci < clipCount; ci++) {
|
|
208
|
+
const animData = BlobStore.getProcessed(`${asset.id}:anim:${ci}`);
|
|
209
|
+
const animMeta = BlobStore.getProcessed(`${asset.id}:animMeta:${ci}`);
|
|
210
|
+
if (animData && animMeta) {
|
|
211
|
+
const meta = JSON.parse(new TextDecoder().decode(animMeta));
|
|
212
|
+
packer.addAsset(meta.stringId, AssetType.AnimationClip, animData, animMeta, []);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return packer.build();
|
|
218
|
+
}
|
|
219
|
+
// ─── Material Assignment Resolution ─────────────────────────────────────────
|
|
220
|
+
function resolveAssignedMaterial(meshAsset, subMeshIndex, sourceMaterialName, assetsById, materialsByStringId) {
|
|
221
|
+
const explicitId = meshAsset.settings.materialOverrides[subMeshIndex];
|
|
222
|
+
if (explicitId !== undefined) {
|
|
223
|
+
if (!explicitId)
|
|
224
|
+
return null;
|
|
225
|
+
const explicitAsset = assetsById.get(explicitId);
|
|
226
|
+
return explicitAsset?.type === AssetType.MaterialSchema ? explicitAsset : null;
|
|
227
|
+
}
|
|
228
|
+
if (!sourceMaterialName)
|
|
229
|
+
return null;
|
|
230
|
+
return materialsByStringId.get(sourceMaterialName.toLowerCase()) ?? null;
|
|
231
|
+
}
|
|
232
|
+
function toMaterialPropertyValue(prop, assetsById) {
|
|
233
|
+
const textureAsset = prop.textureAssetId ? assetsById.get(prop.textureAssetId) : undefined;
|
|
234
|
+
return {
|
|
235
|
+
fieldName: prop.fieldName,
|
|
236
|
+
value: [prop.value[0], prop.value[1], prop.value[2], prop.value[3]],
|
|
237
|
+
textureId: textureAsset?.stringId ?? '',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function applyMaterialAssignments(meshAsset, metadata, assetsById, materialsByStringId) {
|
|
241
|
+
const materials = metadata.materials.map((m) => ({
|
|
242
|
+
name: m.name,
|
|
243
|
+
schemaId: m.schemaId,
|
|
244
|
+
propertyCount: m.properties.length,
|
|
245
|
+
properties: m.properties.map((p) => ({
|
|
246
|
+
fieldName: p.fieldName,
|
|
247
|
+
value: [p.value[0], p.value[1], p.value[2], p.value[3]],
|
|
248
|
+
textureId: p.textureId,
|
|
249
|
+
})),
|
|
250
|
+
}));
|
|
251
|
+
for (let i = 0; i < materials.length; i++) {
|
|
252
|
+
const assigned = resolveAssignedMaterial(meshAsset, i, metadata.materials[i]?.name ?? '', assetsById, materialsByStringId);
|
|
253
|
+
if (!assigned)
|
|
254
|
+
continue;
|
|
255
|
+
const config = assigned.settings.materialConfig;
|
|
256
|
+
const propertyValues = config.properties
|
|
257
|
+
.filter((prop) => prop.fieldName.trim().length > 0)
|
|
258
|
+
.map((prop) => toMaterialPropertyValue(prop, assetsById));
|
|
259
|
+
materials[i].schemaId = config.schemaId || assigned.stringId;
|
|
260
|
+
materials[i].properties = propertyValues;
|
|
261
|
+
materials[i].propertyCount = propertyValues.length;
|
|
262
|
+
}
|
|
263
|
+
return { ...metadata, materials, materialCount: materials.length };
|
|
264
|
+
}
|
|
265
|
+
// ─── Material Schema Metadata Builder ───────────────────────────────────────
|
|
266
|
+
function buildMaterialSchemaMetadata(asset, assetsById) {
|
|
267
|
+
const fields = asset.settings.materialConfig.properties
|
|
268
|
+
.filter((prop) => prop.fieldName.trim().length > 0)
|
|
269
|
+
.map((prop) => {
|
|
270
|
+
const textureAsset = prop.textureAssetId
|
|
271
|
+
? assetsById.get(prop.textureAssetId)
|
|
272
|
+
: undefined;
|
|
273
|
+
return {
|
|
274
|
+
name: prop.fieldName,
|
|
275
|
+
fieldType: prop.fieldType,
|
|
276
|
+
previewFlags: prop.previewFlag,
|
|
277
|
+
defaultValue: [...prop.value],
|
|
278
|
+
defaultTextureId: prop.fieldType === 0 // MaterialFieldType.Texture
|
|
279
|
+
? (textureAsset?.stringId ?? '')
|
|
280
|
+
: '',
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
stringId: asset.stringId,
|
|
285
|
+
schemaName: asset.fileName,
|
|
286
|
+
fieldCount: fields.length,
|
|
287
|
+
fields,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// ─── Animation data blob builder (v2) ────────────────────────────────────────
|
|
291
|
+
export function buildAnimationDataBlobsV2(imported) {
|
|
292
|
+
const results = [];
|
|
293
|
+
for (const clip of imported.animations) {
|
|
294
|
+
const clipTracks = clip.tracks;
|
|
295
|
+
if (!clipTracks || !Array.isArray(clipTracks) || clipTracks.length === 0)
|
|
296
|
+
continue;
|
|
297
|
+
const preparedTracks = [];
|
|
298
|
+
for (const track of clipTracks) {
|
|
299
|
+
const keyframeCount = Math.min(track.times.length, Math.floor(track.values.length / track.valuesPerKey));
|
|
300
|
+
if (keyframeCount <= 0)
|
|
301
|
+
continue;
|
|
302
|
+
const times = track.times.subarray(0, keyframeCount);
|
|
303
|
+
const values = track.values.subarray(0, keyframeCount * track.valuesPerKey);
|
|
304
|
+
// Strip scale tracks
|
|
305
|
+
if (track.name.endsWith('.scale'))
|
|
306
|
+
continue;
|
|
307
|
+
preparedTracks.push({
|
|
308
|
+
trackName: track.name,
|
|
309
|
+
keyframeCount,
|
|
310
|
+
valuesPerKey: track.valuesPerKey,
|
|
311
|
+
times,
|
|
312
|
+
values,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
let dataSize = 0;
|
|
316
|
+
const trackDescriptors = [];
|
|
317
|
+
for (const track of preparedTracks) {
|
|
318
|
+
const timesOffset = dataSize;
|
|
319
|
+
dataSize += track.keyframeCount * 4;
|
|
320
|
+
const valuesOffset = dataSize;
|
|
321
|
+
dataSize += track.keyframeCount * track.valuesPerKey * 4;
|
|
322
|
+
trackDescriptors.push({
|
|
323
|
+
trackName: track.trackName,
|
|
324
|
+
keyframeCount: track.keyframeCount,
|
|
325
|
+
valuesPerKey: track.valuesPerKey,
|
|
326
|
+
timesOffset,
|
|
327
|
+
valuesOffset,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const data = new Uint8Array(dataSize);
|
|
331
|
+
const view = new DataView(data.buffer);
|
|
332
|
+
let offset = 0;
|
|
333
|
+
for (const track of preparedTracks) {
|
|
334
|
+
for (let i = 0; i < track.keyframeCount; i++) {
|
|
335
|
+
view.setFloat32(offset, track.times[i], true);
|
|
336
|
+
offset += 4;
|
|
337
|
+
}
|
|
338
|
+
for (let i = 0; i < track.keyframeCount * track.valuesPerKey; i++) {
|
|
339
|
+
view.setFloat32(offset, track.values[i], true);
|
|
340
|
+
offset += 4;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const metadata = {
|
|
344
|
+
stringId: clip.name || 'default',
|
|
345
|
+
targetMeshId: '',
|
|
346
|
+
metadataVersion: 2,
|
|
347
|
+
duration: clip.duration,
|
|
348
|
+
trackCount: trackDescriptors.length,
|
|
349
|
+
tracks: trackDescriptors,
|
|
350
|
+
};
|
|
351
|
+
results.push({ data, metadata });
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
export interface ServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
wasmDir?: string;
|
|
6
|
+
/** Static apps to serve. Key = URL prefix (e.g. '/packer'), value = directory path. Use '/' for root. */
|
|
7
|
+
staticApps?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function startServer(opts?: ServerOptions): Promise<http.Server>;
|