@series-inc/stowkit-cli 0.1.27 → 0.1.30
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/state.d.ts +2 -0
- package/dist/cli.js +4 -4
- package/dist/core/constants.d.ts +6 -1
- package/dist/core/constants.js +6 -1
- package/dist/core/types.d.ts +2 -1
- package/dist/encoders/fbx-loader.js +136 -43
- package/dist/encoders/glb-loader.js +3 -2
- package/dist/encoders/interfaces.d.ts +7 -0
- package/dist/encoders/interfaces.js +107 -1
- package/dist/encoders/skinned-mesh-builder.js +35 -15
- package/dist/format/metadata.js +2 -2
- package/dist/orchestrator.js +2 -2
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.js +60 -3
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -3
- package/package.json +2 -2
package/dist/app/state.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -226,7 +226,7 @@ async function main() {
|
|
|
226
226
|
process.exit(1);
|
|
227
227
|
}
|
|
228
228
|
const resolvedProject = path.resolve(projectDir);
|
|
229
|
-
await startServer({ port, projectDir: resolvedProject, staticApps: { '/': packerDir } });
|
|
229
|
+
await startServer({ port, projectDir: resolvedProject, force, staticApps: { '/': packerDir } });
|
|
230
230
|
console.log(`\n Packer: http://localhost:${port}\n`);
|
|
231
231
|
openBrowser(`http://localhost:${port}`);
|
|
232
232
|
break;
|
|
@@ -240,11 +240,11 @@ async function main() {
|
|
|
240
240
|
const resolvedProject = path.resolve(projectDir);
|
|
241
241
|
const packerPort = port + 1;
|
|
242
242
|
// Start editor server
|
|
243
|
-
await startServer({ port, projectDir: resolvedProject, staticApps: { '/': editorDir } });
|
|
243
|
+
await startServer({ port, projectDir: resolvedProject, force, staticApps: { '/': editorDir } });
|
|
244
244
|
// Start packer on next port if installed
|
|
245
245
|
const packerDir = resolveAppDir('@series-inc/stowkit-packer-gui', 'stowkit-packer-gui');
|
|
246
246
|
if (packerDir) {
|
|
247
|
-
await startServer({ port: packerPort, projectDir: resolvedProject, staticApps: { '/': packerDir } });
|
|
247
|
+
await startServer({ port: packerPort, projectDir: resolvedProject, force, staticApps: { '/': packerDir } });
|
|
248
248
|
console.log(`\n Editor: http://localhost:${port}`);
|
|
249
249
|
console.log(` Packer: http://localhost:${packerPort}\n`);
|
|
250
250
|
}
|
|
@@ -255,7 +255,7 @@ async function main() {
|
|
|
255
255
|
break;
|
|
256
256
|
}
|
|
257
257
|
case 'serve':
|
|
258
|
-
await startServer({ port, projectDir: projectDir !== '.' ? path.resolve(projectDir) : undefined });
|
|
258
|
+
await startServer({ port, force, projectDir: projectDir !== '.' ? path.resolve(projectDir) : undefined });
|
|
259
259
|
break;
|
|
260
260
|
default:
|
|
261
261
|
console.error(`Unknown command: ${command}`);
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -50,10 +50,15 @@ export declare const SKINNED_MESH_GEOMETRY_INFO_SIZE = 72;
|
|
|
50
50
|
export declare const SKINNED_MESH_METADATA_FIXED_SIZE = 144;
|
|
51
51
|
/** Size of Bone on disk (bytes): name[64] + parent_index(4) + offset_matrix[64] */
|
|
52
52
|
export declare const BONE_SIZE = 132;
|
|
53
|
-
/** Size of VertexWeights on disk (bytes): bone_indices[
|
|
53
|
+
/** Size of VertexWeights on disk (bytes) — legacy: bone_indices[4*u32] + weights[4*f32] */
|
|
54
54
|
export declare const VERTEX_WEIGHTS_SIZE = 32;
|
|
55
|
+
/** Size of compact VertexWeights on disk: bone_indices[4*u16] + weights[4*u8] = 12 bytes */
|
|
56
|
+
export declare const COMPACT_VERTEX_WEIGHTS_SIZE = 12;
|
|
55
57
|
/** Interleaved vertex size for skinned meshes: pos[3] + normal[3] + uv[2] = 8 floats */
|
|
56
58
|
export declare const SKINNED_VERTEX_STRIDE = 32;
|
|
59
|
+
/** SkinnedMeshGeometryInfo flags */
|
|
60
|
+
export declare const SKINNED_INDEX16_FLAG = 1;
|
|
61
|
+
export declare const SKINNED_COMPACT_WEIGHTS_FLAG = 2;
|
|
57
62
|
/** Size of AnimationTrackDescriptor on disk (bytes) */
|
|
58
63
|
export declare const ANIMATION_TRACK_DESCRIPTOR_SIZE = 88;
|
|
59
64
|
/** Size of AnimationClipMetadata fixed portion on disk (bytes) */
|
package/dist/core/constants.js
CHANGED
|
@@ -50,10 +50,15 @@ export const SKINNED_MESH_GEOMETRY_INFO_SIZE = 72;
|
|
|
50
50
|
export const SKINNED_MESH_METADATA_FIXED_SIZE = 144;
|
|
51
51
|
/** Size of Bone on disk (bytes): name[64] + parent_index(4) + offset_matrix[64] */
|
|
52
52
|
export const BONE_SIZE = 132;
|
|
53
|
-
/** Size of VertexWeights on disk (bytes): bone_indices[
|
|
53
|
+
/** Size of VertexWeights on disk (bytes) — legacy: bone_indices[4*u32] + weights[4*f32] */
|
|
54
54
|
export const VERTEX_WEIGHTS_SIZE = 32;
|
|
55
|
+
/** Size of compact VertexWeights on disk: bone_indices[4*u16] + weights[4*u8] = 12 bytes */
|
|
56
|
+
export const COMPACT_VERTEX_WEIGHTS_SIZE = 12;
|
|
55
57
|
/** Interleaved vertex size for skinned meshes: pos[3] + normal[3] + uv[2] = 8 floats */
|
|
56
58
|
export const SKINNED_VERTEX_STRIDE = 32;
|
|
59
|
+
/** SkinnedMeshGeometryInfo flags */
|
|
60
|
+
export const SKINNED_INDEX16_FLAG = 0x1;
|
|
61
|
+
export const SKINNED_COMPACT_WEIGHTS_FLAG = 0x2;
|
|
57
62
|
// ─── Animation V2 Constants ─────────────────────────────────────────────────
|
|
58
63
|
/** Size of AnimationTrackDescriptor on disk (bytes) */
|
|
59
64
|
export const ANIMATION_TRACK_DESCRIPTOR_SIZE = 88;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -159,7 +159,8 @@ export interface SkinnedMeshGeometryInfo {
|
|
|
159
159
|
weightsOffset: number;
|
|
160
160
|
weightsSize: number;
|
|
161
161
|
materialIndex: number;
|
|
162
|
-
|
|
162
|
+
/** Bit flags: 0x1 = index16, 0x2 = compact weights (u16 bone indices + u8 weights) */
|
|
163
|
+
flags: number;
|
|
163
164
|
}
|
|
164
165
|
export interface Bone {
|
|
165
166
|
name: string;
|
|
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
|
|
3
3
|
import { PropertyBinding } from 'three';
|
|
4
4
|
import { parseBinary as fbxParseBinary, parseText as fbxParseText, FBXReader } from 'fbx-parser';
|
|
5
|
+
import { mergeByMaterial, } from './interfaces.js';
|
|
5
6
|
// ─── Minimal DOM shim for Three.js FBXLoader in Node ────────────────────────
|
|
6
7
|
function ensureDomShim() {
|
|
7
8
|
const g = globalThis;
|
|
@@ -62,6 +63,14 @@ export class NodeFbxImporter {
|
|
|
62
63
|
return mesh;
|
|
63
64
|
}
|
|
64
65
|
catch (err) {
|
|
66
|
+
// Animation-only FBX files often crash Three.js's FBXLoader (missing skeleton connections).
|
|
67
|
+
// Fall back to extracting animations directly via fbx-parser.
|
|
68
|
+
const animations = parseFbxAnimations(data);
|
|
69
|
+
if (animations.length > 0) {
|
|
70
|
+
const mesh = emptyMesh();
|
|
71
|
+
mesh.animations = animations;
|
|
72
|
+
return mesh;
|
|
73
|
+
}
|
|
65
74
|
console.error('[NodeFbxImporter] Failed:', err);
|
|
66
75
|
return emptyMesh();
|
|
67
76
|
}
|
|
@@ -188,54 +197,57 @@ function extractMeshFromGroup(group) {
|
|
|
188
197
|
if (!posAttr)
|
|
189
198
|
continue;
|
|
190
199
|
const vertCount = posAttr.count;
|
|
191
|
-
|
|
200
|
+
// Pre-compute full-mesh vertex data (shared across groups)
|
|
201
|
+
const worldMatrix = mesh.matrixWorld;
|
|
202
|
+
const fullPositions = new Float32Array(vertCount * 3);
|
|
192
203
|
{
|
|
193
|
-
const worldMatrix = mesh.matrixWorld;
|
|
194
204
|
const v = new THREE.Vector3();
|
|
195
205
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
196
206
|
v.fromBufferAttribute(posAttr, vi);
|
|
197
207
|
v.applyMatrix4(worldMatrix);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
fullPositions[vi * 3] = v.x;
|
|
209
|
+
fullPositions[vi * 3 + 1] = v.y;
|
|
210
|
+
fullPositions[vi * 3 + 2] = v.z;
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
|
-
let
|
|
213
|
+
let fullNormals = null;
|
|
204
214
|
const normAttr = geometry.getAttribute('normal');
|
|
205
215
|
if (normAttr && normAttr.count === vertCount) {
|
|
206
|
-
|
|
216
|
+
fullNormals = new Float32Array(vertCount * 3);
|
|
207
217
|
const normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
208
218
|
const n = new THREE.Vector3();
|
|
209
219
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
210
220
|
n.fromBufferAttribute(normAttr, vi);
|
|
211
221
|
n.applyMatrix3(normalMatrix).normalize();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
fullNormals[vi * 3] = n.x;
|
|
223
|
+
fullNormals[vi * 3 + 1] = n.y;
|
|
224
|
+
fullNormals[vi * 3 + 2] = n.z;
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
|
-
let
|
|
227
|
+
let fullUvs = null;
|
|
218
228
|
const uvAttr = geometry.getAttribute('uv');
|
|
219
229
|
if (uvAttr && uvAttr.count === vertCount) {
|
|
220
|
-
|
|
230
|
+
fullUvs = new Float32Array(vertCount * 2);
|
|
221
231
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
fullUvs[vi * 2] = uvAttr.getX(vi);
|
|
233
|
+
fullUvs[vi * 2 + 1] = uvAttr.getY(vi);
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
const indexAttr = geometry.getIndex();
|
|
227
|
-
let
|
|
237
|
+
let fullIndices;
|
|
228
238
|
if (indexAttr) {
|
|
229
|
-
|
|
239
|
+
fullIndices = new Uint32Array(indexAttr.count);
|
|
230
240
|
for (let ii = 0; ii < indexAttr.count; ii++)
|
|
231
|
-
|
|
241
|
+
fullIndices[ii] = indexAttr.getX(ii);
|
|
232
242
|
}
|
|
233
243
|
else {
|
|
234
|
-
|
|
244
|
+
fullIndices = new Uint32Array(vertCount);
|
|
235
245
|
for (let ii = 0; ii < vertCount; ii++)
|
|
236
|
-
|
|
246
|
+
fullIndices[ii] = ii;
|
|
237
247
|
}
|
|
238
|
-
|
|
248
|
+
// Pre-compute full skin data
|
|
249
|
+
let fullSkinBoneIndices;
|
|
250
|
+
let fullSkinBoneWeights;
|
|
239
251
|
if (mesh.isSkinnedMesh) {
|
|
240
252
|
const skinIndexAttr = geometry.getAttribute('skinIndex');
|
|
241
253
|
const skinWeightAttr = geometry.getAttribute('skinWeight');
|
|
@@ -249,56 +261,137 @@ function extractMeshFromGroup(group) {
|
|
|
249
261
|
remap[si] = idx >= 0 ? idx : 0;
|
|
250
262
|
}
|
|
251
263
|
}
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
fullSkinBoneIndices = new Uint32Array(vertCount * 4);
|
|
265
|
+
fullSkinBoneWeights = new Float32Array(vertCount * 4);
|
|
254
266
|
for (let vi = 0; vi < vertCount; vi++) {
|
|
255
267
|
const si0 = skinIndexAttr.getX(vi);
|
|
256
268
|
const si1 = skinIndexAttr.getY(vi);
|
|
257
269
|
const si2 = skinIndexAttr.getZ(vi);
|
|
258
270
|
const si3 = skinIndexAttr.getW(vi);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
fullSkinBoneIndices[vi * 4] = remap && si0 < remap.length ? remap[si0] : si0;
|
|
272
|
+
fullSkinBoneIndices[vi * 4 + 1] = remap && si1 < remap.length ? remap[si1] : si1;
|
|
273
|
+
fullSkinBoneIndices[vi * 4 + 2] = remap && si2 < remap.length ? remap[si2] : si2;
|
|
274
|
+
fullSkinBoneIndices[vi * 4 + 3] = remap && si3 < remap.length ? remap[si3] : si3;
|
|
275
|
+
fullSkinBoneWeights[vi * 4] = skinWeightAttr.getX(vi);
|
|
276
|
+
fullSkinBoneWeights[vi * 4 + 1] = skinWeightAttr.getY(vi);
|
|
277
|
+
fullSkinBoneWeights[vi * 4 + 2] = skinWeightAttr.getZ(vi);
|
|
278
|
+
fullSkinBoneWeights[vi * 4 + 3] = skinWeightAttr.getW(vi);
|
|
267
279
|
}
|
|
268
|
-
skinData = { boneIndices, boneWeights };
|
|
269
280
|
}
|
|
270
281
|
}
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
// Determine groups: multi-material meshes have geometry.groups
|
|
283
|
+
const matArray = Array.isArray(mesh.material) ? mesh.material : null;
|
|
284
|
+
const groups = geometry.groups.length > 0 && matArray && matArray.length > 1
|
|
285
|
+
? geometry.groups
|
|
286
|
+
: null;
|
|
287
|
+
const meshSubMeshIndices = [];
|
|
288
|
+
if (groups) {
|
|
289
|
+
// Multi-material: split into one submesh per group
|
|
290
|
+
for (const group of groups) {
|
|
291
|
+
const groupMat = matArray[group.materialIndex ?? 0] ?? matArray[0];
|
|
292
|
+
const matName = groupMat?.name || 'default';
|
|
293
|
+
let matIndex;
|
|
294
|
+
if (materialSet.has(matName)) {
|
|
295
|
+
matIndex = materialSet.get(matName);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
matIndex = materials.length;
|
|
299
|
+
materialSet.set(matName, matIndex);
|
|
300
|
+
materials.push({ name: matName });
|
|
301
|
+
}
|
|
302
|
+
// Extract the index slice for this group and remap vertices
|
|
303
|
+
const groupIndexStart = group.start;
|
|
304
|
+
const groupIndexCount = group.count;
|
|
305
|
+
const usedVerts = new Map(); // old vertex index → new vertex index
|
|
306
|
+
const groupIndicesRaw = [];
|
|
307
|
+
for (let ii = 0; ii < groupIndexCount; ii++) {
|
|
308
|
+
const oldVi = fullIndices[groupIndexStart + ii];
|
|
309
|
+
if (!usedVerts.has(oldVi)) {
|
|
310
|
+
usedVerts.set(oldVi, usedVerts.size);
|
|
311
|
+
}
|
|
312
|
+
groupIndicesRaw.push(usedVerts.get(oldVi));
|
|
313
|
+
}
|
|
314
|
+
const gVertCount = usedVerts.size;
|
|
315
|
+
const positions = new Float32Array(gVertCount * 3);
|
|
316
|
+
const normals = fullNormals ? new Float32Array(gVertCount * 3) : null;
|
|
317
|
+
const uvs = fullUvs ? new Float32Array(gVertCount * 2) : null;
|
|
318
|
+
let skinData;
|
|
319
|
+
let gBoneIndices;
|
|
320
|
+
let gBoneWeights;
|
|
321
|
+
if (fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
322
|
+
gBoneIndices = new Uint32Array(gVertCount * 4);
|
|
323
|
+
gBoneWeights = new Float32Array(gVertCount * 4);
|
|
324
|
+
}
|
|
325
|
+
for (const [oldVi, newVi] of usedVerts) {
|
|
326
|
+
positions[newVi * 3] = fullPositions[oldVi * 3];
|
|
327
|
+
positions[newVi * 3 + 1] = fullPositions[oldVi * 3 + 1];
|
|
328
|
+
positions[newVi * 3 + 2] = fullPositions[oldVi * 3 + 2];
|
|
329
|
+
if (normals && fullNormals) {
|
|
330
|
+
normals[newVi * 3] = fullNormals[oldVi * 3];
|
|
331
|
+
normals[newVi * 3 + 1] = fullNormals[oldVi * 3 + 1];
|
|
332
|
+
normals[newVi * 3 + 2] = fullNormals[oldVi * 3 + 2];
|
|
333
|
+
}
|
|
334
|
+
if (uvs && fullUvs) {
|
|
335
|
+
uvs[newVi * 2] = fullUvs[oldVi * 2];
|
|
336
|
+
uvs[newVi * 2 + 1] = fullUvs[oldVi * 2 + 1];
|
|
337
|
+
}
|
|
338
|
+
if (gBoneIndices && gBoneWeights && fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
339
|
+
gBoneIndices[newVi * 4] = fullSkinBoneIndices[oldVi * 4];
|
|
340
|
+
gBoneIndices[newVi * 4 + 1] = fullSkinBoneIndices[oldVi * 4 + 1];
|
|
341
|
+
gBoneIndices[newVi * 4 + 2] = fullSkinBoneIndices[oldVi * 4 + 2];
|
|
342
|
+
gBoneIndices[newVi * 4 + 3] = fullSkinBoneIndices[oldVi * 4 + 3];
|
|
343
|
+
gBoneWeights[newVi * 4] = fullSkinBoneWeights[oldVi * 4];
|
|
344
|
+
gBoneWeights[newVi * 4 + 1] = fullSkinBoneWeights[oldVi * 4 + 1];
|
|
345
|
+
gBoneWeights[newVi * 4 + 2] = fullSkinBoneWeights[oldVi * 4 + 2];
|
|
346
|
+
gBoneWeights[newVi * 4 + 3] = fullSkinBoneWeights[oldVi * 4 + 3];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (gBoneIndices && gBoneWeights) {
|
|
350
|
+
skinData = { boneIndices: gBoneIndices, boneWeights: gBoneWeights };
|
|
351
|
+
}
|
|
352
|
+
meshSubMeshIndices.push(subMeshes.length);
|
|
353
|
+
subMeshes.push({ skinData, positions, normals, uvs, indices: new Uint32Array(groupIndicesRaw), materialIndex: matIndex });
|
|
354
|
+
}
|
|
276
355
|
}
|
|
277
356
|
else {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
357
|
+
// Single material
|
|
358
|
+
const meshMaterial = matArray ? matArray[0] : mesh.material;
|
|
359
|
+
const matName = meshMaterial?.name || 'default';
|
|
360
|
+
let matIndex;
|
|
361
|
+
if (materialSet.has(matName)) {
|
|
362
|
+
matIndex = materialSet.get(matName);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
matIndex = materials.length;
|
|
366
|
+
materialSet.set(matName, matIndex);
|
|
367
|
+
materials.push({ name: matName });
|
|
368
|
+
}
|
|
369
|
+
let skinData;
|
|
370
|
+
if (fullSkinBoneIndices && fullSkinBoneWeights) {
|
|
371
|
+
skinData = { boneIndices: fullSkinBoneIndices, boneWeights: fullSkinBoneWeights };
|
|
372
|
+
}
|
|
373
|
+
meshSubMeshIndices.push(subMeshes.length);
|
|
374
|
+
subMeshes.push({ skinData, positions: fullPositions, normals: fullNormals, uvs: fullUvs, indices: fullIndices, materialIndex: matIndex });
|
|
281
375
|
}
|
|
282
|
-
subMeshes.push({ skinData, positions, normals, uvs, indices, materialIndex: matIndex });
|
|
283
376
|
nodes.push({
|
|
284
377
|
name: mesh.name || `mesh_${mi}`,
|
|
285
378
|
parentIndex: -1,
|
|
286
379
|
position: [0, 0, 0],
|
|
287
380
|
rotation: [0, 0, 0, 1],
|
|
288
381
|
scale: [1, 1, 1],
|
|
289
|
-
meshIndices:
|
|
382
|
+
meshIndices: meshSubMeshIndices,
|
|
290
383
|
});
|
|
291
384
|
}
|
|
292
385
|
if (materials.length === 0)
|
|
293
386
|
materials.push({ name: 'default' });
|
|
294
|
-
return {
|
|
387
|
+
return mergeByMaterial({
|
|
295
388
|
subMeshes,
|
|
296
389
|
materials,
|
|
297
390
|
nodes,
|
|
298
391
|
hasSkeleton,
|
|
299
392
|
bones: allBones,
|
|
300
393
|
animations: [],
|
|
301
|
-
};
|
|
394
|
+
});
|
|
302
395
|
}
|
|
303
396
|
// ─── Hierarchical Group → ImportedMesh extraction ────────────────────────────
|
|
304
397
|
function extractMeshFromGroupHierarchical(group) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
3
|
+
import { mergeByMaterial } from './interfaces.js';
|
|
3
4
|
import { PropertyBinding } from 'three';
|
|
4
5
|
import { MaterialFieldType, PreviewPropertyFlag } from '../core/types.js';
|
|
5
6
|
// ─── DOM shim (shared pattern from fbx-loader) ─────────────────────────────
|
|
@@ -333,14 +334,14 @@ function extractMeshes(gltf) {
|
|
|
333
334
|
}
|
|
334
335
|
if (materials.length === 0)
|
|
335
336
|
materials.push({ name: 'default' });
|
|
336
|
-
const imported = {
|
|
337
|
+
const imported = mergeByMaterial({
|
|
337
338
|
subMeshes,
|
|
338
339
|
materials,
|
|
339
340
|
nodes,
|
|
340
341
|
hasSkeleton,
|
|
341
342
|
bones: allBones,
|
|
342
343
|
animations: [],
|
|
343
|
-
};
|
|
344
|
+
});
|
|
344
345
|
const meshName = meshObjects[0]?.name || 'mesh';
|
|
345
346
|
return [{ name: meshName, imported, hasSkeleton }];
|
|
346
347
|
}
|
|
@@ -83,6 +83,13 @@ export interface ImportedSkinData {
|
|
|
83
83
|
boneIndices: Uint32Array;
|
|
84
84
|
boneWeights: Float32Array;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
88
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
89
|
+
* Skinned submeshes are left unmerged.
|
|
90
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mergeByMaterial(mesh: ImportedMesh): ImportedMesh;
|
|
86
93
|
export interface DecodedPcm {
|
|
87
94
|
channels: Float32Array[];
|
|
88
95
|
sampleRate: number;
|
|
@@ -1 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
// ─── Mesh Merging (flat mode) ────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Merge submeshes that share the same material into a single submesh.
|
|
4
|
+
* Only used when preserveHierarchy is OFF — vertices are already in world space.
|
|
5
|
+
* Skinned submeshes are left unmerged.
|
|
6
|
+
* Returns a new ImportedMesh with merged submeshes and a single root node.
|
|
7
|
+
*/
|
|
8
|
+
export function mergeByMaterial(mesh) {
|
|
9
|
+
// Group non-skinned submeshes by materialIndex
|
|
10
|
+
const groups = new Map(); // materialIndex → submesh indices
|
|
11
|
+
const skinnedIndices = [];
|
|
12
|
+
for (let i = 0; i < mesh.subMeshes.length; i++) {
|
|
13
|
+
const sm = mesh.subMeshes[i];
|
|
14
|
+
if (sm.skinData) {
|
|
15
|
+
skinnedIndices.push(i);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
let list = groups.get(sm.materialIndex);
|
|
19
|
+
if (!list) {
|
|
20
|
+
list = [];
|
|
21
|
+
groups.set(sm.materialIndex, list);
|
|
22
|
+
}
|
|
23
|
+
list.push(i);
|
|
24
|
+
}
|
|
25
|
+
// Nothing to merge
|
|
26
|
+
if (groups.size === mesh.subMeshes.length - skinnedIndices.length && skinnedIndices.length === 0) {
|
|
27
|
+
// Already one submesh per material, just collapse to single node
|
|
28
|
+
return collapseToSingleNode(mesh);
|
|
29
|
+
}
|
|
30
|
+
const mergedSubMeshes = [];
|
|
31
|
+
// Merge each material group
|
|
32
|
+
for (const [matIdx, smIndices] of groups) {
|
|
33
|
+
if (smIndices.length === 1) {
|
|
34
|
+
mergedSubMeshes.push(mesh.subMeshes[smIndices[0]]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Calculate total sizes
|
|
38
|
+
let totalVerts = 0;
|
|
39
|
+
let totalIndices = 0;
|
|
40
|
+
let hasNormals = true;
|
|
41
|
+
let hasUvs = true;
|
|
42
|
+
for (const si of smIndices) {
|
|
43
|
+
const sm = mesh.subMeshes[si];
|
|
44
|
+
totalVerts += sm.positions.length / 3;
|
|
45
|
+
totalIndices += sm.indices.length;
|
|
46
|
+
if (!sm.normals)
|
|
47
|
+
hasNormals = false;
|
|
48
|
+
if (!sm.uvs)
|
|
49
|
+
hasUvs = false;
|
|
50
|
+
}
|
|
51
|
+
const positions = new Float32Array(totalVerts * 3);
|
|
52
|
+
const normals = hasNormals ? new Float32Array(totalVerts * 3) : null;
|
|
53
|
+
const uvs = hasUvs ? new Float32Array(totalVerts * 2) : null;
|
|
54
|
+
const indices = new Uint32Array(totalIndices);
|
|
55
|
+
let vertOffset = 0;
|
|
56
|
+
let idxOffset = 0;
|
|
57
|
+
for (const si of smIndices) {
|
|
58
|
+
const sm = mesh.subMeshes[si];
|
|
59
|
+
const vc = sm.positions.length / 3;
|
|
60
|
+
positions.set(sm.positions, vertOffset * 3);
|
|
61
|
+
if (normals && sm.normals)
|
|
62
|
+
normals.set(sm.normals, vertOffset * 3);
|
|
63
|
+
if (uvs && sm.uvs)
|
|
64
|
+
uvs.set(sm.uvs, vertOffset * 2);
|
|
65
|
+
// Offset indices to account for merged vertex buffer
|
|
66
|
+
for (let j = 0; j < sm.indices.length; j++) {
|
|
67
|
+
indices[idxOffset + j] = sm.indices[j] + vertOffset;
|
|
68
|
+
}
|
|
69
|
+
vertOffset += vc;
|
|
70
|
+
idxOffset += sm.indices.length;
|
|
71
|
+
}
|
|
72
|
+
mergedSubMeshes.push({ positions, normals, uvs, indices, materialIndex: matIdx });
|
|
73
|
+
}
|
|
74
|
+
// Append skinned submeshes unchanged
|
|
75
|
+
for (const si of skinnedIndices) {
|
|
76
|
+
mergedSubMeshes.push(mesh.subMeshes[si]);
|
|
77
|
+
}
|
|
78
|
+
// Single node referencing all submeshes — the user sees one mesh
|
|
79
|
+
const node = {
|
|
80
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
81
|
+
parentIndex: -1,
|
|
82
|
+
position: [0, 0, 0],
|
|
83
|
+
rotation: [0, 0, 0, 1],
|
|
84
|
+
scale: [1, 1, 1],
|
|
85
|
+
meshIndices: mergedSubMeshes.map((_, i) => i),
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
subMeshes: mergedSubMeshes,
|
|
89
|
+
materials: mesh.materials,
|
|
90
|
+
nodes: [node],
|
|
91
|
+
hasSkeleton: mesh.hasSkeleton,
|
|
92
|
+
bones: mesh.bones,
|
|
93
|
+
animations: mesh.animations,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Collapse N flat nodes into a single node referencing all submeshes */
|
|
97
|
+
function collapseToSingleNode(mesh) {
|
|
98
|
+
const node = {
|
|
99
|
+
name: mesh.nodes[0]?.name || 'mesh',
|
|
100
|
+
parentIndex: -1,
|
|
101
|
+
position: [0, 0, 0],
|
|
102
|
+
rotation: [0, 0, 0, 1],
|
|
103
|
+
scale: [1, 1, 1],
|
|
104
|
+
meshIndices: mesh.subMeshes.map((_, i) => i),
|
|
105
|
+
};
|
|
106
|
+
return { ...mesh, nodes: [node] };
|
|
107
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { SKINNED_VERTEX_STRIDE,
|
|
1
|
+
import { SKINNED_VERTEX_STRIDE, COMPACT_VERTEX_WEIGHTS_SIZE, SKINNED_INDEX16_FLAG, SKINNED_COMPACT_WEIGHTS_FLAG, } from '../core/constants.js';
|
|
2
2
|
export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
3
3
|
const geometries = [];
|
|
4
4
|
let totalDataSize = 0;
|
|
5
5
|
for (const sub of imported.subMeshes) {
|
|
6
6
|
const vertCount = sub.positions.length / 3;
|
|
7
|
+
const useIndex16 = vertCount < 65536;
|
|
7
8
|
const vertexBufSize = vertCount * SKINNED_VERTEX_STRIDE;
|
|
8
|
-
const indexBufSize = sub.indices.length * 4;
|
|
9
|
-
const weightsBufSize = vertCount *
|
|
9
|
+
const indexBufSize = sub.indices.length * (useIndex16 ? 2 : 4);
|
|
10
|
+
const weightsBufSize = vertCount * COMPACT_VERTEX_WEIGHTS_SIZE;
|
|
11
|
+
let flags = SKINNED_COMPACT_WEIGHTS_FLAG;
|
|
12
|
+
if (useIndex16)
|
|
13
|
+
flags |= SKINNED_INDEX16_FLAG;
|
|
10
14
|
geometries.push({
|
|
11
15
|
vertexCount: vertCount,
|
|
12
16
|
indexCount: sub.indices.length,
|
|
@@ -19,15 +23,19 @@ export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
|
19
23
|
weightsOffset: totalDataSize + vertexBufSize + indexBufSize,
|
|
20
24
|
weightsSize: weightsBufSize,
|
|
21
25
|
materialIndex: sub.materialIndex,
|
|
22
|
-
|
|
26
|
+
flags,
|
|
23
27
|
});
|
|
24
28
|
totalDataSize += vertexBufSize + indexBufSize + weightsBufSize;
|
|
25
29
|
}
|
|
26
30
|
const data = new Uint8Array(totalDataSize);
|
|
27
31
|
const view = new DataView(data.buffer);
|
|
28
32
|
let offset = 0;
|
|
29
|
-
for (
|
|
33
|
+
for (let si = 0; si < imported.subMeshes.length; si++) {
|
|
34
|
+
const sub = imported.subMeshes[si];
|
|
35
|
+
const geo = geometries[si];
|
|
30
36
|
const vertCount = sub.positions.length / 3;
|
|
37
|
+
const useIndex16 = (geo.flags & SKINNED_INDEX16_FLAG) !== 0;
|
|
38
|
+
// Vertex buffer (32 bytes per vert: pos[3] + normal[3] + uv[2])
|
|
31
39
|
for (let v = 0; v < vertCount; v++) {
|
|
32
40
|
view.setFloat32(offset, sub.positions[v * 3] * scaleFactor, true);
|
|
33
41
|
offset += 4;
|
|
@@ -64,25 +72,37 @@ export function buildSkinnedMeshData(imported, scaleFactor = 1.0) {
|
|
|
64
72
|
offset += 4;
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
// Index buffer — uint16 or uint32
|
|
76
|
+
if (useIndex16) {
|
|
77
|
+
for (let i = 0; i < sub.indices.length; i++) {
|
|
78
|
+
view.setUint16(offset, sub.indices[i], true);
|
|
79
|
+
offset += 2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
for (let i = 0; i < sub.indices.length; i++) {
|
|
84
|
+
view.setUint32(offset, sub.indices[i], true);
|
|
85
|
+
offset += 4;
|
|
86
|
+
}
|
|
70
87
|
}
|
|
88
|
+
// Compact weights: bone_indices[4*u16] + weights[4*u8] = 12 bytes per vert
|
|
71
89
|
for (let v = 0; v < vertCount; v++) {
|
|
72
90
|
if (sub.skinData) {
|
|
73
91
|
for (let w = 0; w < 4; w++) {
|
|
74
|
-
view.
|
|
75
|
-
offset +=
|
|
92
|
+
view.setUint16(offset, sub.skinData.boneIndices[v * 4 + w], true);
|
|
93
|
+
offset += 2;
|
|
76
94
|
}
|
|
77
95
|
for (let w = 0; w < 4; w++) {
|
|
78
|
-
|
|
79
|
-
offset
|
|
96
|
+
// Quantize float weight [0,1] to uint8 [0,255]
|
|
97
|
+
view.setUint8(offset, Math.round(sub.skinData.boneWeights[v * 4 + w] * 255));
|
|
98
|
+
offset += 1;
|
|
80
99
|
}
|
|
81
100
|
}
|
|
82
101
|
else {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
offset
|
|
102
|
+
// 8 bytes zero indices + 4 bytes zero weights
|
|
103
|
+
for (let w = 0; w < 12; w++) {
|
|
104
|
+
view.setUint8(offset, 0);
|
|
105
|
+
offset += 1;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
}
|
package/dist/format/metadata.js
CHANGED
|
@@ -262,7 +262,7 @@ function serializeSkinnedGeometryInfo(w, g) {
|
|
|
262
262
|
w.writeUint64FromNumber(g.weightsOffset);
|
|
263
263
|
w.writeUint64FromNumber(g.weightsSize);
|
|
264
264
|
w.writeUint32(g.materialIndex);
|
|
265
|
-
w.writeUint32(g.
|
|
265
|
+
w.writeUint32(g.flags);
|
|
266
266
|
}
|
|
267
267
|
function deserializeSkinnedGeometryInfo(r) {
|
|
268
268
|
return {
|
|
@@ -277,7 +277,7 @@ function deserializeSkinnedGeometryInfo(r) {
|
|
|
277
277
|
weightsOffset: r.readUint64AsNumber(),
|
|
278
278
|
weightsSize: r.readUint64AsNumber(),
|
|
279
279
|
materialIndex: r.readUint32(),
|
|
280
|
-
|
|
280
|
+
flags: r.readUint32(),
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
function serializeBone(w, bone) {
|
package/dist/orchestrator.js
CHANGED
|
@@ -263,10 +263,10 @@ export async function fullBuild(projectDir, opts) {
|
|
|
263
263
|
for (let si = 0; si < mesh.imported.subMeshes.length; si++) {
|
|
264
264
|
const matIdx = mesh.imported.subMeshes[si].materialIndex;
|
|
265
265
|
const matName = mesh.imported.materials[matIdx]?.name;
|
|
266
|
-
if (matName) {
|
|
266
|
+
if (matName && overrides[matIdx] === undefined) {
|
|
267
267
|
const matChildId = `${container.id}/${matName}.stowmat`;
|
|
268
268
|
if (assetsById.has(matChildId)) {
|
|
269
|
-
overrides[
|
|
269
|
+
overrides[matIdx] = matChildId;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ProcessResult {
|
|
|
22
22
|
processedSize: number;
|
|
23
23
|
glbChildren?: GlbChildDescriptor[];
|
|
24
24
|
glbExtract?: GlbExtractResult;
|
|
25
|
+
warnings?: string[];
|
|
25
26
|
}
|
|
26
27
|
export declare function processAsset(id: string, sourceData: Uint8Array, type: AssetType, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
27
28
|
export declare function processExtractedMesh(childId: string, imported: ImportedMesh, hasSkeleton: boolean, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
|
|
@@ -40,3 +41,4 @@ export declare function buildAnimationDataBlobsV2(imported: ImportedMesh): {
|
|
|
40
41
|
data: Uint8Array;
|
|
41
42
|
metadata: AnimationClipMetadata;
|
|
42
43
|
}[];
|
|
44
|
+
export declare function generateAnimationWarnings(imported: ImportedMesh): string[];
|
package/dist/pipeline.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
99
99
|
log('importing FBX...');
|
|
100
100
|
const imported = await ctx.meshImporter.import(sourceData, id);
|
|
101
101
|
const animCount = imported.animations?.length ?? 0;
|
|
102
|
+
const animWarnings = generateAnimationWarnings(imported);
|
|
102
103
|
if (animCount === 0)
|
|
103
104
|
throw new Error('No animation clips found in FBX file');
|
|
104
105
|
const animBlobs = buildAnimationDataBlobsV2(imported);
|
|
@@ -117,7 +118,9 @@ export async function processAsset(id, sourceData, type, stringId, settings, ctx
|
|
|
117
118
|
BlobStore.setProcessed(`${id}:animCount`, new Uint8Array([animBlobs.length]));
|
|
118
119
|
const totalAnimBytes = animBlobs.reduce((s, a) => s + a.data.length, 0);
|
|
119
120
|
log(`animation serialized (${(totalAnimBytes / 1024).toFixed(0)} KB, ${animBlobs.length} clips)`);
|
|
120
|
-
|
|
121
|
+
if (animWarnings.length > 0)
|
|
122
|
+
log(`warnings: ${animWarnings.join('; ')}`);
|
|
123
|
+
return { metadata: primary.metadata, processedSize: totalAnimBytes, warnings: animWarnings.length > 0 ? animWarnings : undefined };
|
|
121
124
|
}
|
|
122
125
|
if (type === AssetType.Audio) {
|
|
123
126
|
log('decoding audio...');
|
|
@@ -367,8 +370,14 @@ function resolveAssignedMaterial(meshAsset, subMeshIndex, sourceMaterialName, as
|
|
|
367
370
|
}
|
|
368
371
|
function toMaterialPropertyValue(prop, assetsById) {
|
|
369
372
|
const textureAsset = prop.textureAssetId ? assetsById.get(prop.textureAssetId) : undefined;
|
|
373
|
+
// Use previewFlag name as fallback when fieldName is empty (e.g. alphaTest)
|
|
374
|
+
const flagNames = {
|
|
375
|
+
1: 'mainTex', 2: 'tint', 3: 'alphaTest',
|
|
376
|
+
4: 'metalness', 5: 'roughness', 6: 'normalMap', 7: 'emissiveMap', 8: 'emissive',
|
|
377
|
+
};
|
|
378
|
+
const fieldName = prop.fieldName.trim() || flagNames[prop.previewFlag] || '';
|
|
370
379
|
return {
|
|
371
|
-
fieldName
|
|
380
|
+
fieldName,
|
|
372
381
|
value: [prop.value[0], prop.value[1], prop.value[2], prop.value[3]],
|
|
373
382
|
textureId: textureAsset?.stringId ?? '',
|
|
374
383
|
};
|
|
@@ -390,7 +399,7 @@ function applyMaterialAssignments(meshAsset, metadata, assetsById, materialsBySt
|
|
|
390
399
|
continue;
|
|
391
400
|
const config = assigned.settings.materialConfig;
|
|
392
401
|
const propertyValues = config.properties
|
|
393
|
-
.filter((prop) => prop.fieldName.trim().length > 0)
|
|
402
|
+
.filter((prop) => prop.fieldName.trim().length > 0 || prop.previewFlag > 0)
|
|
394
403
|
.map((prop) => toMaterialPropertyValue(prop, assetsById));
|
|
395
404
|
materials[i].schemaId = config.schemaId || assigned.stringId;
|
|
396
405
|
materials[i].properties = propertyValues;
|
|
@@ -488,3 +497,51 @@ export function buildAnimationDataBlobsV2(imported) {
|
|
|
488
497
|
}
|
|
489
498
|
return results;
|
|
490
499
|
}
|
|
500
|
+
// ─── Animation Diagnostics ──────────────────────────────────────────────────
|
|
501
|
+
export function generateAnimationWarnings(imported) {
|
|
502
|
+
const warnings = [];
|
|
503
|
+
if (!imported.animations || imported.animations.length === 0) {
|
|
504
|
+
warnings.push('No animation clips found in FBX');
|
|
505
|
+
return warnings;
|
|
506
|
+
}
|
|
507
|
+
const boneNames = new Set((imported.bones ?? []).map(b => b.name));
|
|
508
|
+
for (const clip of imported.animations) {
|
|
509
|
+
const prefix = imported.animations.length > 1 ? `Clip "${clip.name}": ` : '';
|
|
510
|
+
if (!clip.tracks || clip.tracks.length === 0) {
|
|
511
|
+
warnings.push(`${prefix}No animation tracks`);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (clip.duration <= 0) {
|
|
515
|
+
warnings.push(`${prefix}Duration is 0`);
|
|
516
|
+
}
|
|
517
|
+
// Check for bone name mismatches
|
|
518
|
+
const trackBoneNames = new Set();
|
|
519
|
+
let singleKeyframeTracks = 0;
|
|
520
|
+
for (const track of clip.tracks) {
|
|
521
|
+
if (track.name.endsWith('.scale'))
|
|
522
|
+
continue;
|
|
523
|
+
const boneName = track.name.split('.')[0];
|
|
524
|
+
trackBoneNames.add(boneName);
|
|
525
|
+
if (track.times.length <= 1)
|
|
526
|
+
singleKeyframeTracks++;
|
|
527
|
+
}
|
|
528
|
+
if (boneNames.size > 0 && trackBoneNames.size > 0) {
|
|
529
|
+
const unmatched = [];
|
|
530
|
+
for (const tn of trackBoneNames) {
|
|
531
|
+
if (!boneNames.has(tn))
|
|
532
|
+
unmatched.push(tn);
|
|
533
|
+
}
|
|
534
|
+
if (unmatched.length > 0 && unmatched.length === trackBoneNames.size) {
|
|
535
|
+
warnings.push(`${prefix}No track bone names match skeleton (${unmatched.length} unmatched: ${unmatched.slice(0, 3).join(', ')}${unmatched.length > 3 ? '...' : ''})`);
|
|
536
|
+
}
|
|
537
|
+
else if (unmatched.length > 0) {
|
|
538
|
+
warnings.push(`${prefix}${unmatched.length}/${trackBoneNames.size} track bones not in skeleton: ${unmatched.slice(0, 3).join(', ')}${unmatched.length > 3 ? '...' : ''}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const nonScaleTracks = clip.tracks.filter(t => !t.name.endsWith('.scale'));
|
|
542
|
+
if (singleKeyframeTracks > 0 && singleKeyframeTracks === nonScaleTracks.length) {
|
|
543
|
+
warnings.push(`${prefix}All ${singleKeyframeTracks} tracks have only 1 keyframe (static pose)`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return warnings;
|
|
547
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ServerOptions {
|
|
|
3
3
|
port?: number;
|
|
4
4
|
projectDir?: string;
|
|
5
5
|
wasmDir?: string;
|
|
6
|
+
/** Force reprocess all assets on startup, ignoring cache. */
|
|
7
|
+
force?: boolean;
|
|
6
8
|
/** Static apps to serve. Key = URL prefix (e.g. '/packer'), value = directory path. Use '/' for root. */
|
|
7
9
|
staticApps?: Record<string, string>;
|
|
8
10
|
}
|
package/dist/server.js
CHANGED
|
@@ -327,10 +327,10 @@ async function processGlbContainer(containerId) {
|
|
|
327
327
|
for (let si = 0; si < mesh.imported.subMeshes.length; si++) {
|
|
328
328
|
const matIdx = mesh.imported.subMeshes[si].materialIndex;
|
|
329
329
|
const matName = mesh.imported.materials[matIdx]?.name;
|
|
330
|
-
if (matName) {
|
|
330
|
+
if (matName && overrides[matIdx] === undefined) {
|
|
331
331
|
const matChildId = `${containerId}/${matName}.stowmat`;
|
|
332
332
|
if (childAssets.some(a => a.id === matChildId)) {
|
|
333
|
-
overrides[
|
|
333
|
+
overrides[matIdx] = matChildId;
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -735,6 +735,7 @@ async function processOneAsset(id) {
|
|
|
735
735
|
asset.metadata = result.metadata;
|
|
736
736
|
asset.processedSize = result.processedSize;
|
|
737
737
|
asset.sourceSize = sourceData.length;
|
|
738
|
+
asset.warnings = result.warnings;
|
|
738
739
|
broadcast({
|
|
739
740
|
type: 'asset-update', id,
|
|
740
741
|
updates: {
|
|
@@ -742,6 +743,7 @@ async function processOneAsset(id) {
|
|
|
742
743
|
metadata: result.metadata,
|
|
743
744
|
processedSize: result.processedSize,
|
|
744
745
|
sourceSize: sourceData.length,
|
|
746
|
+
warnings: result.warnings,
|
|
745
747
|
},
|
|
746
748
|
});
|
|
747
749
|
}
|
|
@@ -1576,7 +1578,7 @@ export async function startServer(opts = {}) {
|
|
|
1576
1578
|
// Open project if specified (skip if already loaded — e.g. stowkit editor starts two servers)
|
|
1577
1579
|
if (opts.projectDir && (!projectConfig || projectConfig.projectDir !== opts.projectDir)) {
|
|
1578
1580
|
await openProject(opts.projectDir);
|
|
1579
|
-
queueProcessing();
|
|
1581
|
+
queueProcessing({ force: opts.force });
|
|
1580
1582
|
}
|
|
1581
1583
|
return new Promise((resolve) => {
|
|
1582
1584
|
server.listen(port, () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/stowkit-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@series-inc/stowkit-packer-gui": "^0.1.
|
|
20
|
+
"@series-inc/stowkit-packer-gui": "^0.1.16",
|
|
21
21
|
"@series-inc/stowkit-editor": "^0.1.2",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|