@stowkit/three-loader 0.1.14 → 0.1.16

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.
@@ -47,16 +47,13 @@ class MeshParser {
47
47
  if (metadataBlob.length < 140) {
48
48
  throw new Error(`Metadata blob too small: ${metadataBlob.length} bytes (expected at least 140)`);
49
49
  }
50
- console.log(`Parsing mesh metadata: ${metadataBlob.length} bytes metadata, ${dataBlob.length} bytes data`);
51
50
  const metadata = this.parseMeshMetadata(metadataBlob);
52
- console.log(`Mesh counts: ${metadata.meshGeometryCount} geometries, ${metadata.materialCount} materials, ${metadata.nodeCount} nodes`);
53
51
  // Check if the metadata blob contains just the header or all the mesh info
54
52
  const expectedMetadataSize = 140 +
55
53
  (metadata.meshGeometryCount * 40) + // Draco compressed (40 bytes each)
56
54
  (metadata.materialCount * 196) + // MaterialData header
57
55
  (metadata.nodeCount * 116);
58
56
  let hasExtendedMetadata = metadataBlob.length >= expectedMetadataSize;
59
- console.log(`Expected metadata size: ${expectedMetadataSize}, actual: ${metadataBlob.length}, has extended: ${hasExtendedMetadata}`);
60
57
  // If metadata only has the header, the mesh info might be at the start of the data blob
61
58
  const sourceBlob = hasExtendedMetadata ? metadataBlob : dataBlob;
62
59
  const metaView = new DataView(sourceBlob.buffer, sourceBlob.byteOffset, sourceBlob.byteLength);
@@ -102,7 +99,6 @@ class MeshParser {
102
99
  }
103
100
  // Parse nodes first (they come before material properties in the blob)
104
101
  const nodes = [];
105
- console.log(`\n=== Nodes from StowKit file (${metadata.nodeCount} total) ===`);
106
102
  for (let i = 0; i < metadata.nodeCount; i++) {
107
103
  if (offset + 116 > sourceBlob.length) {
108
104
  console.warn(`Data truncated at node ${i}/${metadata.nodeCount}`);
@@ -130,11 +126,9 @@ class MeshParser {
130
126
  meshCount: metaView.getUint32(offset + 108, true),
131
127
  firstMeshIndex: metaView.getUint32(offset + 112, true)
132
128
  };
133
- console.log(`Node ${i}: "${node.name}" (parent=${node.parentIndex}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex})`);
134
129
  nodes.push(node);
135
130
  offset += 116; // Size of Node struct
136
131
  }
137
- console.log(`=== End of nodes from file ===\n`);
138
132
  // Parse mesh indices
139
133
  const totalMeshRefs = nodes.reduce((sum, node) => sum + node.meshCount, 0);
140
134
  console.log(`Parsing ${totalMeshRefs} mesh indices at offset ${offset}`);
@@ -153,7 +147,6 @@ class MeshParser {
153
147
  // MaterialPropertyValue: field_name[64] + value[4*4=16] + texture_id[64] = 144 bytes
154
148
  for (let i = 0; i < materialData.length; i++) {
155
149
  const matData = materialData[i];
156
- console.log(`Material "${matData.name}": schema="${matData.schemaId}", ${matData.propertyCount} properties`);
157
150
  for (let j = 0; j < matData.propertyCount; j++) {
158
151
  if (offset + 144 > sourceBlob.length) {
159
152
  console.warn(`Data truncated at property ${j}/${matData.propertyCount} of material ${i}`);
@@ -172,7 +165,6 @@ class MeshParser {
172
165
  value,
173
166
  textureId
174
167
  });
175
- console.log(` Property "${fieldName}": value=[${value.join(', ')}], textureId="${textureId}"`);
176
168
  offset += 144; // Size of MaterialPropertyValue
177
169
  }
178
170
  // Create Three.js material based on properties
@@ -182,16 +174,13 @@ class MeshParser {
182
174
  // Start with default gray color
183
175
  color: new THREE__namespace.Color(0.8, 0.8, 0.8)
184
176
  });
185
- console.log(`Creating material ${i}: "${matData.name}", ${matData.propertyCount} properties`);
186
177
  // Apply properties
187
178
  for (const prop of matData.properties) {
188
179
  if (prop.fieldName.toLowerCase() === 'tint' || prop.fieldName.toLowerCase().includes('color')) {
189
180
  // Apply color/tint
190
181
  mat.color = new THREE__namespace.Color(prop.value[0], prop.value[1], prop.value[2]);
191
- console.log(` Applied tint color: rgb(${prop.value[0]}, ${prop.value[1]}, ${prop.value[2]})`);
192
182
  }
193
183
  }
194
- console.log(` Material ${i} final color: rgb(${mat.color.r}, ${mat.color.g}, ${mat.color.b})`);
195
184
  materials.push(mat);
196
185
  }
197
186
  // Default material if none exist
@@ -209,14 +198,6 @@ class MeshParser {
209
198
  * Create Three.js BufferGeometry from Draco compressed mesh data
210
199
  */
211
200
  static async createGeometry(geoInfo, dataBlob, dracoLoader) {
212
- console.log('Decoding Draco geometry:', {
213
- vertexCount: geoInfo.vertexCount,
214
- indexCount: geoInfo.indexCount,
215
- hasNormals: geoInfo.hasNormals,
216
- hasUVs: geoInfo.hasUVs,
217
- compressedOffset: geoInfo.compressedBufferOffset,
218
- compressedSize: geoInfo.compressedBufferSize
219
- });
220
201
  // Extract the Draco compressed buffer
221
202
  if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
222
203
  throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
@@ -230,8 +211,7 @@ class MeshParser {
230
211
  const geometry = await new Promise((resolve, reject) => {
231
212
  dracoLoader.load(url, (decoded) => {
232
213
  URL.revokeObjectURL(url);
233
- console.log('Draco decoded successfully:', decoded);
234
- // UVs are pre-flipped in the packer now, no need to flip here
214
+ // UVs are pre-flipped in the packer now
235
215
  resolve(decoded);
236
216
  }, undefined, (error) => {
237
217
  URL.revokeObjectURL(url);
@@ -242,13 +222,6 @@ class MeshParser {
242
222
  // Compute bounding volumes
243
223
  geometry.computeBoundingSphere();
244
224
  geometry.computeBoundingBox();
245
- console.log('Final geometry:', {
246
- hasPositions: !!geometry.attributes.position,
247
- hasNormals: !!geometry.attributes.normal,
248
- hasUVs: !!geometry.attributes.uv,
249
- hasIndices: !!geometry.index,
250
- vertexCount: geometry.attributes.position?.count || 0
251
- });
252
225
  return geometry;
253
226
  }
254
227
  /**
@@ -258,18 +231,11 @@ class MeshParser {
258
231
  const root = new THREE__namespace.Group();
259
232
  root.name = 'StowKitMesh';
260
233
  const { geometries, materials, nodes, meshIndices } = parsedData;
261
- console.log('Building scene with:', {
262
- geometries: geometries.length,
263
- materials: materials.length,
264
- nodes: nodes.length,
265
- meshIndices: meshIndices.length
266
- });
267
234
  // Create all Three.js objects for nodes
268
235
  const nodeObjects = [];
269
236
  for (const node of nodes) {
270
237
  const obj = new THREE__namespace.Group();
271
238
  obj.name = node.name;
272
- console.log(`Node ${node.name}: position=${node.position}, rotation=${node.rotation}, scale=${node.scale}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex}`);
273
239
  // Apply transform
274
240
  obj.position.set(...node.position);
275
241
  obj.quaternion.set(...node.rotation);
@@ -278,10 +244,8 @@ class MeshParser {
278
244
  for (let i = 0; i < node.meshCount; i++) {
279
245
  const meshIndexArrayPos = node.firstMeshIndex + i;
280
246
  const meshIndex = meshIndices[meshIndexArrayPos];
281
- console.log(` meshIndices[${meshIndexArrayPos}] = ${meshIndex} (adding to node ${node.name})`);
282
247
  if (meshIndex < geometries.length) {
283
248
  const geoInfo = geometries[meshIndex];
284
- console.log(` Creating geometry from geoInfo: vertices=${geoInfo.vertexCount}, indices=${geoInfo.indexCount}, material=${geoInfo.materialIndex}`);
285
249
  const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
286
250
  // Use assigned material if valid, otherwise create default
287
251
  let material;
@@ -299,35 +263,28 @@ class MeshParser {
299
263
  }
300
264
  const mesh = new THREE__namespace.Mesh(geometry, material);
301
265
  obj.add(mesh);
302
- console.log(` ✓ Added mesh to node ${node.name} (${geometry.attributes.position?.count || 0} vertices, material: ${material.name || 'default'})`);
303
266
  }
304
267
  else {
305
- console.error(`Mesh index ${meshIndex} out of bounds (have ${geometries.length} geometries)`);
268
+ console.error(`Mesh index ${meshIndex} out of bounds (${geometries.length} geometries available)`);
306
269
  }
307
270
  }
308
- console.log(` Node ${node.name} now has ${obj.children.length} children`);
309
271
  nodeObjects.push(obj);
310
272
  }
311
273
  // Build hierarchy
312
- console.log('Building node hierarchy...');
313
274
  for (let i = 0; i < nodes.length; i++) {
314
275
  const node = nodes[i];
315
276
  const obj = nodeObjects[i];
316
277
  if (node.parentIndex >= 0 && node.parentIndex < nodeObjects.length) {
317
- console.log(` Adding node "${node.name}" (index ${i}) as child of node "${nodes[node.parentIndex].name}" (index ${node.parentIndex})`);
318
278
  nodeObjects[node.parentIndex].add(obj);
319
279
  }
320
280
  else {
321
- console.log(` Adding node "${node.name}" (index ${i}) as direct child of root (parentIndex=${node.parentIndex})`);
322
281
  root.add(obj);
323
282
  }
324
283
  }
325
284
  // If no nodes, create a single mesh from all geometries
326
285
  if (nodes.length === 0 && geometries.length > 0) {
327
- console.log('No nodes found, creating meshes directly from geometries');
328
286
  for (let index = 0; index < geometries.length; index++) {
329
287
  const geoInfo = geometries[index];
330
- console.log(`Creating direct mesh ${index}: materialIndex=${geoInfo.materialIndex}, vertices=${geoInfo.vertexCount}`);
331
288
  const geometry = await this.createGeometry(geoInfo, dataBlob, dracoLoader);
332
289
  // Use assigned material if valid, otherwise create default
333
290
  let material;
@@ -346,33 +303,8 @@ class MeshParser {
346
303
  const mesh = new THREE__namespace.Mesh(geometry, material);
347
304
  mesh.name = `Mesh_${index}`;
348
305
  root.add(mesh);
349
- console.log(`Added mesh ${index} to root`);
350
306
  }
351
307
  }
352
- console.log(`Final scene has ${root.children.length} direct children`);
353
- // Debug: traverse and count all meshes, showing full hierarchy
354
- let totalMeshCount = 0;
355
- let hierarchyLog = 'Scene hierarchy:\n';
356
- root.traverse((obj) => {
357
- // Calculate depth by counting parents
358
- let depth = 0;
359
- let parent = obj.parent;
360
- while (parent && parent !== root) {
361
- depth++;
362
- parent = parent.parent;
363
- }
364
- const indent = ' '.repeat(depth);
365
- if (obj instanceof THREE__namespace.Mesh) {
366
- totalMeshCount++;
367
- const mat = obj.material;
368
- hierarchyLog += `${indent}[Mesh] vertices=${obj.geometry.attributes.position?.count || 0}, material=${mat.name}, hasTexture=${!!mat.map}\n`;
369
- }
370
- else if (obj instanceof THREE__namespace.Group && obj !== root) {
371
- hierarchyLog += `${indent}[Group] ${obj.name} (${obj.children.length} children)\n`;
372
- }
373
- });
374
- console.log(hierarchyLog);
375
- console.log(`Total meshes in scene: ${totalMeshCount}`);
376
308
  return root;
377
309
  }
378
310
  /**
@@ -395,6 +327,317 @@ class StowKitPack {
395
327
  this.ktx2Loader = ktx2Loader;
396
328
  this.dracoLoader = dracoLoader;
397
329
  }
330
+ /**
331
+ * Load a skinned mesh by its string ID
332
+ */
333
+ async loadSkinnedMesh(stringId) {
334
+ const assetIndex = this.reader.findAssetByPath(stringId);
335
+ if (assetIndex < 0) {
336
+ throw new Error(`Skinned mesh not found: ${stringId}`);
337
+ }
338
+ const data = this.reader.readAssetData(assetIndex);
339
+ const metadata = this.reader.readAssetMetadata(assetIndex);
340
+ if (!data)
341
+ throw new Error(`Failed to read skinned mesh data: ${stringId}`);
342
+ if (!metadata)
343
+ throw new Error(`No metadata for skinned mesh: ${stringId}`);
344
+ return await this.parseSkinnedMesh(metadata, data);
345
+ }
346
+ /**
347
+ * Parse skinned mesh from binary data
348
+ */
349
+ async parseSkinnedMesh(metadata, data) {
350
+ // Parse skinned mesh metadata
351
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
352
+ const decoder = new TextDecoder();
353
+ // SkinnedMeshMetadata: 4 uint32s (16 bytes) THEN string_id[128] = 144 bytes total
354
+ const meshGeometryCount = view.getUint32(0, true);
355
+ const materialCount = view.getUint32(4, true);
356
+ const nodeCount = view.getUint32(8, true);
357
+ const boneCount = view.getUint32(12, true);
358
+ const stringIdBytes = metadata.slice(16, 144);
359
+ const stringIdNullIndex = stringIdBytes.indexOf(0);
360
+ const stringId = decoder
361
+ .decode(stringIdBytes.slice(0, stringIdNullIndex >= 0 ? stringIdNullIndex : 128))
362
+ .trim();
363
+ let offset = 144;
364
+ // Parse all geometry infos
365
+ const geometryInfos = [];
366
+ // SkinnedMeshGeometryInfo: 4 uint32s + 6 uint64s + 2 uint32s = 16 + 48 + 8 = 72 bytes
367
+ // Layout: vertex_count, index_count, has_normals, has_uvs (16 bytes)
368
+ // vertex_buffer_offset, vertex_buffer_size, index_buffer_offset, index_buffer_size, weights_offset, weights_size (48 bytes)
369
+ // material_index, _padding (8 bytes)
370
+ for (let g = 0; g < meshGeometryCount; g++) {
371
+ if (offset + 72 > metadata.length) {
372
+ throw new Error(`Metadata too small for geometry ${g}`);
373
+ }
374
+ geometryInfos.push({
375
+ vertexCount: view.getUint32(offset, true),
376
+ indexCount: view.getUint32(offset + 4, true),
377
+ hasNormals: view.getUint32(offset + 8, true),
378
+ hasUVs: view.getUint32(offset + 12, true),
379
+ vertexBufferOffset: Number(view.getBigUint64(offset + 16, true)),
380
+ vertexBufferSize: Number(view.getBigUint64(offset + 24, true)),
381
+ indexBufferOffset: Number(view.getBigUint64(offset + 32, true)),
382
+ indexBufferSize: Number(view.getBigUint64(offset + 40, true)),
383
+ weightsOffset: Number(view.getBigUint64(offset + 48, true)),
384
+ weightsSize: Number(view.getBigUint64(offset + 56, true)),
385
+ materialIndex: view.getUint32(offset + 64, true)
386
+ // _padding at offset + 68 (ignored)
387
+ });
388
+ offset += 72;
389
+ }
390
+ const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
391
+ // Sort geometry infos by vertex buffer offset to ensure sequential parsing
392
+ const sortedGeometryInfos = geometryInfos.slice().sort((a, b) => a.vertexBufferOffset - b.vertexBufferOffset);
393
+ // Parse materials from metadata (immediately after geometries)
394
+ let metadataOffset = 144 + meshGeometryCount * 72;
395
+ const materialData = [];
396
+ // Parse MaterialData headers
397
+ for (let i = 0; i < materialCount; i++) {
398
+ if (metadataOffset + 196 > metadata.length) {
399
+ console.warn('Skinned mesh metadata truncated while reading material headers');
400
+ break;
401
+ }
402
+ const nameBytes = metadata.slice(metadataOffset, metadataOffset + 64);
403
+ const schemaIdBytes = metadata.slice(metadataOffset + 64, metadataOffset + 192);
404
+ const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
405
+ const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
406
+ const propertyCount = view.getUint32(metadataOffset + 192, true);
407
+ materialData.push({ name, schemaId, propertyCount, properties: [] });
408
+ metadataOffset += 196;
409
+ }
410
+ // Skip Node array and node_mesh_indices (not needed for rendering)
411
+ // Node struct: transform[16 floats] + name[64 bytes] + mesh_index[4 bytes] + parent_index[4 bytes] = 116 bytes
412
+ const NODE_STRIDE = 116;
413
+ metadataOffset += nodeCount * NODE_STRIDE;
414
+ metadataOffset += nodeCount * 4;
415
+ // Parse material properties
416
+ const PROPERTY_STRIDE = 144;
417
+ for (let i = 0; i < materialCount; i++) {
418
+ const mat = materialData[i];
419
+ for (let p = 0; p < mat.propertyCount; p++) {
420
+ try {
421
+ if (metadataOffset + PROPERTY_STRIDE > metadata.length) {
422
+ break;
423
+ }
424
+ const fieldNameBytes = metadata.slice(metadataOffset, metadataOffset + 64);
425
+ const fieldName = decoder
426
+ .decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64))
427
+ .trim();
428
+ const value = [
429
+ view.getFloat32(metadataOffset + 64, true),
430
+ view.getFloat32(metadataOffset + 68, true),
431
+ view.getFloat32(metadataOffset + 72, true),
432
+ view.getFloat32(metadataOffset + 76, true)
433
+ ];
434
+ const textureIdBytes = metadata.slice(metadataOffset + 80, metadataOffset + 144);
435
+ const textureIdNullIndex = textureIdBytes.indexOf(0);
436
+ const textureId = (textureIdNullIndex >= 0
437
+ ? decoder.decode(textureIdBytes.slice(0, textureIdNullIndex))
438
+ : decoder.decode(textureIdBytes))
439
+ .trim();
440
+ mat.properties.push({ fieldName, value, textureId });
441
+ metadataOffset += PROPERTY_STRIDE;
442
+ }
443
+ catch (err) {
444
+ throw err;
445
+ }
446
+ }
447
+ }
448
+ // Parse bones
449
+ const BONE_STRIDE = 64 + 4 + 16 * 4;
450
+ const bones = [];
451
+ const parentIndices = [];
452
+ const boneMatrices = [];
453
+ const boneOriginalNames = [];
454
+ const bindPoses = [];
455
+ for (let i = 0; i < boneCount; i++) {
456
+ if (metadataOffset + BONE_STRIDE > metadata.length) {
457
+ throw new Error(`Skinned mesh metadata truncated while reading bone ${i}`);
458
+ }
459
+ const nameBytes = new Uint8Array(metadata.buffer, metadata.byteOffset + metadataOffset, 64);
460
+ const nullIndex = nameBytes.indexOf(0);
461
+ const rawName = decoder.decode(nameBytes.subarray(0, nullIndex >= 0 ? nullIndex : 64)).trim();
462
+ const parentIndex = view.getInt32(metadataOffset + 64, true);
463
+ const offsetMatrix = new THREE__namespace.Matrix4();
464
+ const matrixElements = new Float32Array(16);
465
+ for (let j = 0; j < 16; j++) {
466
+ matrixElements[j] = view.getFloat32(metadataOffset + 68 + j * 4, true);
467
+ }
468
+ offsetMatrix.fromArray(matrixElements);
469
+ const sanitizedName = this.sanitizeTrackName(rawName);
470
+ const bone = new THREE__namespace.Bone();
471
+ bone.name = sanitizedName;
472
+ bone.userData.originalName = rawName;
473
+ bones.push(bone);
474
+ parentIndices.push(parentIndex);
475
+ boneMatrices.push(offsetMatrix);
476
+ boneOriginalNames.push(rawName);
477
+ bindPoses.push(offsetMatrix.clone().invert());
478
+ metadataOffset += BONE_STRIDE;
479
+ }
480
+ // Build bone hierarchy
481
+ for (let i = 0; i < bones.length; i++) {
482
+ const parentIndex = parentIndices[i];
483
+ if (parentIndex >= 0 && parentIndex < bones.length && parentIndex !== i) {
484
+ bones[parentIndex].add(bones[i]);
485
+ }
486
+ }
487
+ // Set bones to bind pose for correct static display
488
+ for (let i = 0; i < bones.length; i++) {
489
+ const bone = bones[i];
490
+ const bindPose = bindPoses[i];
491
+ const parentIndex = parentIndices[i];
492
+ if (parentIndex < 0) {
493
+ const pos = new THREE__namespace.Vector3();
494
+ const quat = new THREE__namespace.Quaternion();
495
+ const scale = new THREE__namespace.Vector3();
496
+ bindPose.decompose(pos, quat, scale);
497
+ bone.position.copy(pos);
498
+ bone.quaternion.copy(quat);
499
+ bone.scale.set(1, 1, 1);
500
+ }
501
+ else {
502
+ const parentBindPose = bindPoses[parentIndex];
503
+ const parentInv = new THREE__namespace.Matrix4().copy(parentBindPose).invert();
504
+ const localMatrix = new THREE__namespace.Matrix4().multiplyMatrices(parentInv, bindPose);
505
+ const pos = new THREE__namespace.Vector3();
506
+ const quat = new THREE__namespace.Quaternion();
507
+ const scale = new THREE__namespace.Vector3();
508
+ localMatrix.decompose(pos, quat, scale);
509
+ bone.position.copy(pos);
510
+ bone.quaternion.copy(quat);
511
+ bone.scale.set(1, 1, 1);
512
+ }
513
+ bone.updateMatrix();
514
+ }
515
+ // Create Three.js materials with properties (same as static meshes)
516
+ const materials = [];
517
+ for (let i = 0; i < materialCount; i++) {
518
+ const mat = materialData[i];
519
+ const material = new THREE__namespace.MeshStandardMaterial({
520
+ side: THREE__namespace.DoubleSide,
521
+ name: mat.name || `Material_${i}`,
522
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8) // Default gray
523
+ });
524
+ // Apply non-texture properties
525
+ for (const prop of mat.properties) {
526
+ const fieldName = prop.fieldName.toLowerCase();
527
+ // Handle color properties
528
+ if (fieldName.includes('color') || fieldName.includes('tint')) {
529
+ material.color.setRGB(prop.value[0], prop.value[1], prop.value[2]);
530
+ }
531
+ else if (fieldName === 'metallic') {
532
+ material.metalness = prop.value[0];
533
+ }
534
+ else if (fieldName === 'roughness') {
535
+ material.roughness = prop.value[0];
536
+ }
537
+ }
538
+ materials.push(material);
539
+ }
540
+ await this.loadMaterialTextures(materialData, materials);
541
+ // Collect root bones (hierarchy already built earlier)
542
+ const rootBones = bones.filter((_, index) => parentIndices[index] < 0);
543
+ // Create shared skeleton
544
+ const skeleton = new THREE__namespace.Skeleton(bones, boneMatrices);
545
+ // Pre-allocate combined buffers
546
+ const totalVertexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.vertexCount, 0);
547
+ const totalIndexCount = sortedGeometryInfos.reduce((sum, info) => sum + info.indexCount, 0);
548
+ const combinedPositions = new Float32Array(totalVertexCount * 3);
549
+ const combinedNormals = new Float32Array(totalVertexCount * 3);
550
+ const combinedUVs = new Float32Array(totalVertexCount * 2);
551
+ const combinedSkinIndices = new Uint16Array(totalVertexCount * 4);
552
+ const combinedSkinWeights = new Float32Array(totalVertexCount * 4);
553
+ const combinedIndices = new Uint32Array(totalIndexCount);
554
+ const combinedGeometry = new THREE__namespace.BufferGeometry();
555
+ combinedGeometry.clearGroups();
556
+ let vertexCursor = 0; // counts vertices
557
+ let indexCursor = 0; // counts indices
558
+ for (const geomInfo of sortedGeometryInfos) {
559
+ const vertexStrideBytes = 32;
560
+ const vertexBase = vertexCursor; // base vertex index for this submesh
561
+ // Validate offsets before parsing
562
+ const requiredVertexBytes = geomInfo.vertexBufferOffset + geomInfo.vertexCount * vertexStrideBytes;
563
+ const requiredIndexBytes = geomInfo.indexBufferOffset + geomInfo.indexCount * 4;
564
+ const requiredWeightBytes = geomInfo.weightsOffset + geomInfo.vertexCount * 32;
565
+ if (requiredVertexBytes > data.byteLength) {
566
+ throw new Error(`Vertex buffer out of bounds: need ${requiredVertexBytes}, have ${data.byteLength}`);
567
+ }
568
+ if (requiredIndexBytes > data.byteLength) {
569
+ throw new Error(`Index buffer out of bounds: need ${requiredIndexBytes}, have ${data.byteLength}`);
570
+ }
571
+ if (requiredWeightBytes > data.byteLength) {
572
+ throw new Error(`Weight buffer out of bounds: need ${requiredWeightBytes}, have ${data.byteLength}`);
573
+ }
574
+ // Parse vertices
575
+ for (let v = 0; v < geomInfo.vertexCount; v++) {
576
+ const vertexOffset = geomInfo.vertexBufferOffset + v * vertexStrideBytes;
577
+ // positions
578
+ combinedPositions[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 0, true);
579
+ combinedPositions[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 4, true);
580
+ combinedPositions[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 8, true);
581
+ // normals
582
+ if (geomInfo.hasNormals) {
583
+ combinedNormals[(vertexCursor + v) * 3 + 0] = dataView.getFloat32(vertexOffset + 12, true);
584
+ combinedNormals[(vertexCursor + v) * 3 + 1] = dataView.getFloat32(vertexOffset + 16, true);
585
+ combinedNormals[(vertexCursor + v) * 3 + 2] = dataView.getFloat32(vertexOffset + 20, true);
586
+ }
587
+ // uvs
588
+ if (geomInfo.hasUVs) {
589
+ combinedUVs[(vertexCursor + v) * 2 + 0] = dataView.getFloat32(vertexOffset + 24, true);
590
+ combinedUVs[(vertexCursor + v) * 2 + 1] = dataView.getFloat32(vertexOffset + 28, true);
591
+ }
592
+ // bone indices & weights
593
+ const weightOffset = geomInfo.weightsOffset + v * 32;
594
+ for (let j = 0; j < 4; j++) {
595
+ const boneIndex = dataView.getUint32(weightOffset + j * 4, true);
596
+ combinedSkinIndices[(vertexCursor + v) * 4 + j] = boneIndex < boneCount ? boneIndex : 0;
597
+ }
598
+ for (let j = 0; j < 4; j++) {
599
+ combinedSkinWeights[(vertexCursor + v) * 4 + j] = dataView.getFloat32(weightOffset + 16 + j * 4, true);
600
+ }
601
+ }
602
+ // Parse indices
603
+ const groupStart = indexCursor;
604
+ for (let i = 0; i < geomInfo.indexCount; i++) {
605
+ const indexValue = dataView.getUint32(geomInfo.indexBufferOffset + i * 4, true);
606
+ combinedIndices[indexCursor + i] = indexValue + vertexBase;
607
+ }
608
+ combinedGeometry.addGroup(groupStart, geomInfo.indexCount, Math.min(geomInfo.materialIndex, materials.length - 1));
609
+ vertexCursor += geomInfo.vertexCount;
610
+ indexCursor += geomInfo.indexCount;
611
+ }
612
+ combinedGeometry.setAttribute('position', new THREE__namespace.BufferAttribute(combinedPositions, 3));
613
+ if (geometryInfos.some(info => info.hasNormals)) {
614
+ combinedGeometry.setAttribute('normal', new THREE__namespace.BufferAttribute(combinedNormals, 3));
615
+ }
616
+ if (geometryInfos.some(info => info.hasUVs)) {
617
+ combinedGeometry.setAttribute('uv', new THREE__namespace.BufferAttribute(combinedUVs, 2));
618
+ }
619
+ combinedGeometry.setAttribute('skinIndex', new THREE__namespace.Uint16BufferAttribute(combinedSkinIndices, 4));
620
+ combinedGeometry.setAttribute('skinWeight', new THREE__namespace.BufferAttribute(combinedSkinWeights, 4));
621
+ combinedGeometry.setIndex(new THREE__namespace.BufferAttribute(combinedIndices, 1));
622
+ combinedGeometry.computeBoundingBox();
623
+ combinedGeometry.computeBoundingSphere();
624
+ const skinnedMesh = new THREE__namespace.SkinnedMesh(combinedGeometry, materials);
625
+ // Create a separate bone container to preserve hierarchy
626
+ const boneContainer = new THREE__namespace.Group();
627
+ boneContainer.name = 'BoneContainer';
628
+ rootBones.forEach((root) => boneContainer.add(root));
629
+ // Bind skeleton with our provided inverse bind matrices
630
+ skinnedMesh.bind(skeleton, skinnedMesh.matrixWorld);
631
+ skinnedMesh.normalizeSkinWeights();
632
+ skinnedMesh.updateMatrixWorld(true);
633
+ const container = new THREE__namespace.Group();
634
+ container.name = stringId || 'SkinnedMesh';
635
+ container.add(boneContainer);
636
+ container.add(skinnedMesh);
637
+ container.userData.boneOriginalNames = boneOriginalNames;
638
+ container.userData.skinnedMesh = skinnedMesh;
639
+ return container;
640
+ }
398
641
  /**
399
642
  * Load a mesh by its canonical path/name
400
643
  */
@@ -492,10 +735,16 @@ class StowKitPack {
492
735
  return this.reader.readAssetMetadata(index);
493
736
  }
494
737
  /**
495
- * Parse texture metadata
738
+ * Load skinned mesh by index
496
739
  */
497
- parseTextureMetadata(metadataBytes) {
498
- return this.reader.parseTextureMetadata(metadataBytes);
740
+ async loadSkinnedMeshByIndex(index) {
741
+ const data = this.reader.readAssetData(index);
742
+ const metadata = this.reader.readAssetMetadata(index);
743
+ if (!data)
744
+ throw new Error(`Failed to read skinned mesh data for index ${index}`);
745
+ if (!metadata)
746
+ throw new Error(`No metadata for skinned mesh index ${index}`);
747
+ return await this.parseSkinnedMesh(metadata, data);
499
748
  }
500
749
  /**
501
750
  * Load mesh by index
@@ -537,56 +786,167 @@ class StowKitPack {
537
786
  return audio;
538
787
  }
539
788
  /**
540
- * Get audio metadata by path
789
+ * Get audio metadata by path (uses WASM parsing)
541
790
  */
542
791
  getAudioMetadata(assetPath) {
543
792
  const assetIndex = this.reader.findAssetByPath(assetPath);
544
793
  if (assetIndex < 0)
545
794
  return null;
546
- const metadata = this.reader.readAssetMetadata(assetIndex);
547
- if (!metadata || metadata.length < 140)
548
- return null;
549
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
550
- const decoder = new TextDecoder();
551
- const stringIdBytes = metadata.slice(0, 128);
552
- const nullIdx = stringIdBytes.indexOf(0);
553
- const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
554
- return {
555
- stringId,
556
- sampleRate: view.getUint32(128, true),
557
- channels: view.getUint32(132, true),
558
- durationMs: view.getUint32(136, true)
559
- };
795
+ return this.reader.parseAudioMetadata(assetIndex);
560
796
  }
561
797
  /**
562
- * Load material schema by index
798
+ * Get animation metadata by index (uses WASM parsing)
563
799
  */
564
- loadMaterialSchemaByIndex(index) {
565
- return this.loadMaterialSchemaByIndex_internal(index);
800
+ getAnimationMetadata(index) {
801
+ return this.reader.parseAnimationMetadata(index);
566
802
  }
567
803
  /**
568
- * Get mesh materials by index
804
+ * Get texture metadata by index (uses WASM parsing)
569
805
  */
570
- getMeshMaterialsByIndex(index) {
571
- return this.getMeshMaterialsByIndex_internal(index);
806
+ getTextureMetadata(index) {
807
+ return this.reader.parseTextureMetadata(index);
572
808
  }
573
809
  /**
574
- * Get material schema information
810
+ * Load animation clip by string ID
575
811
  */
576
- getMaterialSchema(assetPath) {
577
- const assetIndex = this.reader.findAssetByPath(assetPath);
578
- if (assetIndex < 0)
579
- return null;
580
- return this.loadMaterialSchemaByIndex_internal(assetIndex);
812
+ async loadAnimationClip(stringId) {
813
+ const index = this.reader.findAssetByPath(stringId);
814
+ if (index < 0) {
815
+ throw new Error(`Animation clip not found: ${stringId}`);
816
+ }
817
+ return this.loadAnimationClipByIndex(index);
581
818
  }
582
819
  /**
583
- * Get mesh materials information
820
+ * Load and play animation on a skinned mesh group
584
821
  */
585
- getMeshMaterials(assetPath) {
586
- const assetIndex = this.reader.findAssetByPath(assetPath);
587
- if (assetIndex < 0)
588
- return null;
589
- return this.getMeshMaterialsByIndex_internal(assetIndex);
822
+ async loadAnimation(skinnedMeshGroup, animationPath) {
823
+ const clip = await this.loadAnimationClip(animationPath);
824
+ const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
825
+ const action = mixer.clipAction(clip);
826
+ action.setLoop(THREE__namespace.LoopRepeat, Infinity);
827
+ action.play();
828
+ return { mixer, action, clip };
829
+ }
830
+ /**
831
+ * Load and play animation by index
832
+ */
833
+ async loadAnimationByIndex(skinnedMeshGroup, animationIndex) {
834
+ const clip = await this.loadAnimationClipByIndex(animationIndex);
835
+ const mixer = new THREE__namespace.AnimationMixer(skinnedMeshGroup);
836
+ const action = mixer.clipAction(clip);
837
+ action.setLoop(THREE__namespace.LoopRepeat, Infinity);
838
+ action.play();
839
+ return { mixer, action, clip };
840
+ }
841
+ /**
842
+ * Load animation clip by index
843
+ */
844
+ async loadAnimationClipByIndex(index) {
845
+ const metadata = this.reader.readAssetMetadata(index);
846
+ const data = this.reader.readAssetData(index);
847
+ if (!metadata || !data) {
848
+ throw new Error('Failed to read animation clip data');
849
+ }
850
+ return this.parseAnimationClip(metadata, data);
851
+ }
852
+ /**
853
+ * Parse animation clip from binary data
854
+ */
855
+ parseAnimationClip(metadata, data) {
856
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
857
+ const decoder = new TextDecoder();
858
+ // Parse AnimationClipMetadata (272 bytes total)
859
+ const stringIdBytes = metadata.slice(0, 128);
860
+ const stringIdNullIndex = stringIdBytes.indexOf(0);
861
+ const stringId = decoder.decode(stringIdBytes.slice(0, stringIdNullIndex >= 0 ? stringIdNullIndex : 128));
862
+ const targetMeshIdBytes = metadata.slice(128, 256);
863
+ const targetMeshIdNullIndex = targetMeshIdBytes.indexOf(0);
864
+ decoder
865
+ .decode(targetMeshIdBytes.slice(0, targetMeshIdNullIndex >= 0 ? targetMeshIdNullIndex : 128))
866
+ .trim();
867
+ const durationSeconds = view.getFloat32(256, true); // Duration in seconds (per C header)
868
+ view.getFloat32(260, true); // For custom playback rate if needed
869
+ const channelCount = view.getUint32(264, true);
870
+ const boneCount = view.getUint32(268, true);
871
+ // Parse AnimationChannel[] (starts at offset 272, each is 28 bytes)
872
+ const channelsOffset = 272;
873
+ const channels = [];
874
+ for (let i = 0; i < channelCount; i++) {
875
+ const chOffset = channelsOffset + i * 28;
876
+ channels.push({
877
+ boneIndex: view.getUint32(chOffset, true),
878
+ keyframeType: view.getUint32(chOffset + 4, true),
879
+ keyframeCount: view.getUint32(chOffset + 8, true),
880
+ keyframeTimesOffset: Number(view.getBigUint64(chOffset + 12, true)),
881
+ keyframeValuesOffset: Number(view.getBigUint64(chOffset + 20, true))
882
+ });
883
+ }
884
+ // Parse AnimationBoneInfo[] (starts after all channels, each is 68 bytes)
885
+ const bonesOffset = channelsOffset + channelCount * 28;
886
+ const bones = [];
887
+ for (let i = 0; i < boneCount; i++) {
888
+ const boneOffset = bonesOffset + i * 68;
889
+ const nameBytes = metadata.slice(boneOffset, boneOffset + 64);
890
+ const nullIndex = nameBytes.indexOf(0);
891
+ const rawName = decoder
892
+ .decode(nameBytes.slice(0, nullIndex >= 0 ? nullIndex : 64))
893
+ .trim();
894
+ const sanitizedName = this.sanitizeTrackName(rawName);
895
+ const parentIndex = view.getInt32(boneOffset + 64, true);
896
+ bones.push({ name: rawName, parentIndex, sanitizedName });
897
+ }
898
+ // Create DataView for keyframe data
899
+ const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
900
+ // Create Three.js KeyframeTracks
901
+ const tracks = [];
902
+ let maxKeyframeTime = 0;
903
+ // Process channels - each channel specifies which bone it animates via boneIndex
904
+ for (let i = 0; i < channelCount; i++) {
905
+ const channel = channels[i];
906
+ // Validate bone index
907
+ if (channel.boneIndex >= boneCount) {
908
+ console.warn(`[AnimationClip] Channel ${i} references invalid bone index ${channel.boneIndex} (have ${boneCount} bones)`);
909
+ continue;
910
+ }
911
+ const boneName = bones[channel.boneIndex].sanitizedName;
912
+ // Skip scale tracks - bones should always have unit scale
913
+ if (channel.keyframeType === 2) {
914
+ continue;
915
+ }
916
+ // Read times (already in seconds per C header)
917
+ const times = this.readFloatArray(dataView, channel.keyframeTimesOffset, channel.keyframeCount);
918
+ // Track the maximum keyframe time to calculate actual duration
919
+ if (times.length > 0) {
920
+ const channelMaxTime = times[times.length - 1];
921
+ if (channelMaxTime > maxKeyframeTime) {
922
+ maxKeyframeTime = channelMaxTime;
923
+ }
924
+ }
925
+ // Create track based on keyframe type
926
+ if (channel.keyframeType === 0) {
927
+ // Position (vec3)
928
+ const values = this.readFloatArray(dataView, channel.keyframeValuesOffset, channel.keyframeCount * 3);
929
+ tracks.push(new THREE__namespace.VectorKeyframeTrack(`${boneName}.position`, times, values));
930
+ }
931
+ else if (channel.keyframeType === 1) {
932
+ // Rotation (quaternion)
933
+ const values = this.readFloatArray(dataView, channel.keyframeValuesOffset, channel.keyframeCount * 4);
934
+ tracks.push(new THREE__namespace.QuaternionKeyframeTrack(`${boneName}.quaternion`, times, values));
935
+ }
936
+ }
937
+ // Use the actual maximum keyframe time as duration instead of metadata duration
938
+ const actualDuration = maxKeyframeTime > 0 ? maxKeyframeTime : durationSeconds;
939
+ return new THREE__namespace.AnimationClip(stringId, actualDuration, tracks);
940
+ }
941
+ /**
942
+ * Helper to read float array from DataView
943
+ */
944
+ readFloatArray(view, offset, count) {
945
+ const result = new Float32Array(count);
946
+ for (let i = 0; i < count; i++) {
947
+ result[i] = view.getFloat32(offset + i * 4, true);
948
+ }
949
+ return result;
590
950
  }
591
951
  /**
592
952
  * Close the pack and free resources
@@ -600,13 +960,15 @@ class StowKitPack {
600
960
  const matData = materialData[i];
601
961
  const material = materials[i];
602
962
  for (const prop of matData.properties) {
603
- if (prop.textureId && prop.textureId.length > 0) {
963
+ // Trim and check for truly non-empty texture ID
964
+ const textureId = prop.textureId?.trim();
965
+ if (textureId && textureId.length > 0) {
604
966
  try {
605
- const texture = await this.loadTexture(prop.textureId);
967
+ const texture = await this.loadTexture(textureId);
606
968
  this.applyTextureToMaterial(material, prop.fieldName, texture);
607
969
  }
608
970
  catch (error) {
609
- console.error(`Failed to load texture "${prop.textureId}":`, error);
971
+ console.error(`Failed to load texture "${textureId}":`, error);
610
972
  }
611
973
  }
612
974
  }
@@ -653,88 +1015,11 @@ class StowKitPack {
653
1015
  }
654
1016
  material.needsUpdate = true;
655
1017
  }
656
- loadMaterialSchemaByIndex_internal(index) {
657
- const metadata = this.reader.readAssetMetadata(index);
658
- if (!metadata || metadata.length < 196)
659
- return null;
660
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
661
- const decoder = new TextDecoder();
662
- const stringIdBytes = metadata.slice(0, 128);
663
- const schemaNameBytes = metadata.slice(128, 192);
664
- const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
665
- const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
666
- const fieldCount = view.getUint32(192, true);
667
- const fields = [];
668
- let offset = 196;
669
- for (let i = 0; i < fieldCount; i++) {
670
- if (offset + 88 > metadata.length)
671
- break;
672
- const fieldNameBytes = metadata.slice(offset, offset + 64);
673
- const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
674
- const fieldType = view.getUint32(offset + 64, true);
675
- const previewFlags = view.getUint32(offset + 68, true);
676
- const defaultValue = [
677
- view.getFloat32(offset + 72, true),
678
- view.getFloat32(offset + 76, true),
679
- view.getFloat32(offset + 80, true),
680
- view.getFloat32(offset + 84, true)
681
- ];
682
- const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
683
- const previewFlagNames = ['None', 'MainTex', 'Tint'];
684
- fields.push({
685
- name: fieldName,
686
- type: typeNames[fieldType] || 'Float',
687
- previewFlag: previewFlagNames[previewFlags] || 'None',
688
- defaultValue
689
- });
690
- offset += 88;
691
- }
692
- return { stringId, schemaName, fields };
693
- }
694
- getMeshMaterialsByIndex_internal(index) {
695
- const metadata = this.reader.readAssetMetadata(index);
696
- if (!metadata || metadata.length < 140)
697
- return null;
698
- const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
699
- const decoder = new TextDecoder();
700
- const meshGeometryCount = view.getUint32(0, true);
701
- const materialCount = view.getUint32(4, true);
702
- const nodeCount = view.getUint32(8, true);
703
- let offset = 140;
704
- offset += meshGeometryCount * 40;
705
- const materials = [];
706
- for (let i = 0; i < materialCount; i++) {
707
- if (offset + 196 > metadata.length)
708
- break;
709
- const nameBytes = metadata.slice(offset, offset + 64);
710
- const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
711
- const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
712
- const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
713
- const propertyCount = view.getUint32(offset + 192, true);
714
- materials.push({ name, schemaId, propertyCount, properties: [] });
715
- offset += 196;
716
- }
717
- offset += nodeCount * 116;
718
- offset += nodeCount * 4;
719
- for (const mat of materials) {
720
- for (let j = 0; j < mat.propertyCount; j++) {
721
- if (offset + 144 > metadata.length)
722
- break;
723
- const fieldNameBytes = metadata.slice(offset, offset + 64);
724
- const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
725
- const value = [
726
- view.getFloat32(offset + 64, true),
727
- view.getFloat32(offset + 68, true),
728
- view.getFloat32(offset + 72, true),
729
- view.getFloat32(offset + 76, true)
730
- ];
731
- const textureIdBytes = metadata.slice(offset + 80, offset + 144);
732
- const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
733
- mat.properties.push({ fieldName, value, textureId });
734
- offset += 144;
735
- }
736
- }
737
- return materials;
1018
+ sanitizeTrackName(name) {
1019
+ return name
1020
+ .trim()
1021
+ .replace(/[^A-Za-z0-9_\-]+/g, '_')
1022
+ .replace(/_{2,}/g, '_');
738
1023
  }
739
1024
  }
740
1025
 
@@ -782,7 +1067,10 @@ class StowKitLoader {
782
1067
  if (data instanceof ArrayBuffer) {
783
1068
  arrayBuffer = data;
784
1069
  }
785
- else if (data instanceof Blob || data instanceof File) {
1070
+ else if (typeof Blob !== 'undefined' && data instanceof Blob) {
1071
+ arrayBuffer = await data.arrayBuffer();
1072
+ }
1073
+ else if ('arrayBuffer' in data && typeof data.arrayBuffer === 'function') {
786
1074
  arrayBuffer = await data.arrayBuffer();
787
1075
  }
788
1076
  else {
@@ -821,6 +1109,10 @@ StowKitLoader.ktx2Loader = null;
821
1109
  StowKitLoader.dracoLoader = null;
822
1110
  StowKitLoader.initialized = false;
823
1111
 
1112
+ Object.defineProperty(exports, 'AssetType', {
1113
+ enumerable: true,
1114
+ get: function () { return reader.AssetType; }
1115
+ });
824
1116
  exports.StowKitLoader = StowKitLoader;
825
1117
  exports.StowKitPack = StowKitPack;
826
1118
  //# sourceMappingURL=stowkit-three-loader.js.map