@stowkit/three-loader 0.1.2

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.
@@ -0,0 +1,1034 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+ var KTX2Loader_js = require('three/examples/jsm/loaders/KTX2Loader.js');
5
+ var reader = require('@stowkit/reader');
6
+
7
+ function _interopNamespaceDefault(e) {
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
25
+
26
+ class MeshParser {
27
+ /**
28
+ * Parse mesh metadata from binary data
29
+ */
30
+ static parseMeshMetadata(data) {
31
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
32
+ // MeshMetadata structure (140 bytes total when packed)
33
+ const metadata = {
34
+ meshGeometryCount: view.getUint32(0, true),
35
+ materialCount: view.getUint32(4, true),
36
+ nodeCount: view.getUint32(8, true),
37
+ stringId: this.readString(data.slice(12, 140)) // 128 bytes for string
38
+ };
39
+ return metadata;
40
+ }
41
+ /**
42
+ * Parse complete mesh data including geometry, materials, and nodes
43
+ */
44
+ static parseMeshData(metadataBlob, dataBlob) {
45
+ // Ensure we have at least the minimum metadata size
46
+ if (metadataBlob.length < 140) {
47
+ throw new Error(`Metadata blob too small: ${metadataBlob.length} bytes (expected at least 140)`);
48
+ }
49
+ console.log(`Parsing mesh metadata: ${metadataBlob.length} bytes metadata, ${dataBlob.length} bytes data`);
50
+ const metadata = this.parseMeshMetadata(metadataBlob);
51
+ console.log(`Mesh counts: ${metadata.meshGeometryCount} geometries, ${metadata.materialCount} materials, ${metadata.nodeCount} nodes`);
52
+ // Check if the metadata blob contains just the header or all the mesh info
53
+ const expectedMetadataSize = 140 +
54
+ (metadata.meshGeometryCount * 76) +
55
+ (metadata.materialCount * 64) +
56
+ (metadata.nodeCount * 116);
57
+ let hasExtendedMetadata = metadataBlob.length >= expectedMetadataSize;
58
+ console.log(`Expected metadata size: ${expectedMetadataSize}, actual: ${metadataBlob.length}, has extended: ${hasExtendedMetadata}`);
59
+ // If metadata only has the header, the mesh info might be at the start of the data blob
60
+ const sourceBlob = hasExtendedMetadata ? metadataBlob : dataBlob;
61
+ const metaView = new DataView(sourceBlob.buffer, sourceBlob.byteOffset, sourceBlob.byteLength);
62
+ let offset = hasExtendedMetadata ? 140 : 0; // Start after MeshMetadata if in metadata blob, or at beginning of data blob
63
+ // Parse geometry infos
64
+ const geometries = [];
65
+ for (let i = 0; i < metadata.meshGeometryCount; i++) {
66
+ if (offset + 76 > sourceBlob.length) {
67
+ console.warn(`Data truncated at geometry ${i}/${metadata.meshGeometryCount}`);
68
+ break;
69
+ }
70
+ const geo = {
71
+ vertexCount: metaView.getUint32(offset, true),
72
+ indexCount: metaView.getUint32(offset + 4, true),
73
+ vertexBufferOffset: Number(metaView.getBigUint64(offset + 8, true)),
74
+ indexBufferOffset: Number(metaView.getBigUint64(offset + 16, true)),
75
+ normalBufferOffset: Number(metaView.getBigUint64(offset + 24, true)),
76
+ uvBufferOffset: Number(metaView.getBigUint64(offset + 32, true)),
77
+ vertexBufferSize: Number(metaView.getBigUint64(offset + 40, true)),
78
+ indexBufferSize: Number(metaView.getBigUint64(offset + 48, true)),
79
+ normalBufferSize: Number(metaView.getBigUint64(offset + 56, true)),
80
+ uvBufferSize: Number(metaView.getBigUint64(offset + 64, true)),
81
+ materialIndex: metaView.getUint32(offset + 72, true)
82
+ };
83
+ geometries.push(geo);
84
+ offset += 76; // Size of MeshGeometryInfo
85
+ }
86
+ // Parse materials - MaterialData structure: name[64] + schema_id[128] + property_count(4) = 196 bytes
87
+ const materialData = [];
88
+ const materials = [];
89
+ for (let i = 0; i < metadata.materialCount; i++) {
90
+ if (offset + 196 > sourceBlob.length) {
91
+ console.warn(`Data truncated at material ${i}/${metadata.materialCount}`);
92
+ break;
93
+ }
94
+ const name = this.readString(sourceBlob.slice(offset, offset + 64));
95
+ const schemaId = this.readString(sourceBlob.slice(offset + 64, offset + 192));
96
+ const propertyCount = metaView.getUint32(offset + 192, true);
97
+ materialData.push({
98
+ name,
99
+ schemaId,
100
+ propertyCount,
101
+ properties: []
102
+ });
103
+ offset += 196; // Size of MaterialData header
104
+ }
105
+ // Parse nodes first (they come before material properties in the blob)
106
+ const nodes = [];
107
+ console.log(`\n=== Nodes from StowKit file (${metadata.nodeCount} total) ===`);
108
+ for (let i = 0; i < metadata.nodeCount; i++) {
109
+ if (offset + 116 > sourceBlob.length) {
110
+ console.warn(`Data truncated at node ${i}/${metadata.nodeCount}`);
111
+ break;
112
+ }
113
+ const node = {
114
+ name: this.readString(sourceBlob.slice(offset, offset + 64)),
115
+ parentIndex: metaView.getInt32(offset + 64, true),
116
+ position: [
117
+ metaView.getFloat32(offset + 68, true),
118
+ metaView.getFloat32(offset + 72, true),
119
+ metaView.getFloat32(offset + 76, true)
120
+ ],
121
+ rotation: [
122
+ metaView.getFloat32(offset + 80, true),
123
+ metaView.getFloat32(offset + 84, true),
124
+ metaView.getFloat32(offset + 88, true),
125
+ metaView.getFloat32(offset + 92, true)
126
+ ],
127
+ scale: [
128
+ metaView.getFloat32(offset + 96, true),
129
+ metaView.getFloat32(offset + 100, true),
130
+ metaView.getFloat32(offset + 104, true)
131
+ ],
132
+ meshCount: metaView.getUint32(offset + 108, true),
133
+ firstMeshIndex: metaView.getUint32(offset + 112, true)
134
+ };
135
+ console.log(`Node ${i}: "${node.name}" (parent=${node.parentIndex}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex})`);
136
+ nodes.push(node);
137
+ offset += 116; // Size of Node struct
138
+ }
139
+ console.log(`=== End of nodes from file ===\n`);
140
+ // Parse mesh indices
141
+ const totalMeshRefs = nodes.reduce((sum, node) => sum + node.meshCount, 0);
142
+ console.log(`Parsing ${totalMeshRefs} mesh indices at offset ${offset}`);
143
+ const meshIndices = new Uint32Array(totalMeshRefs);
144
+ for (let i = 0; i < totalMeshRefs; i++) {
145
+ if (offset + (i + 1) * 4 > sourceBlob.length) {
146
+ console.warn(`Data truncated at mesh index ${i}/${totalMeshRefs}`);
147
+ break;
148
+ }
149
+ meshIndices[i] = metaView.getUint32(offset + i * 4, true);
150
+ console.log(` meshIndices[${i}] = ${meshIndices[i]}`);
151
+ }
152
+ offset += totalMeshRefs * 4;
153
+ console.log(`After mesh indices, offset = ${offset}`);
154
+ // Second pass: read material properties
155
+ // MaterialPropertyValue: field_name[64] + value[4*4=16] + texture_id[64] = 144 bytes
156
+ for (let i = 0; i < materialData.length; i++) {
157
+ const matData = materialData[i];
158
+ console.log(`Material "${matData.name}": schema="${matData.schemaId}", ${matData.propertyCount} properties`);
159
+ for (let j = 0; j < matData.propertyCount; j++) {
160
+ if (offset + 144 > sourceBlob.length) {
161
+ console.warn(`Data truncated at property ${j}/${matData.propertyCount} of material ${i}`);
162
+ break;
163
+ }
164
+ const fieldName = this.readString(sourceBlob.slice(offset, offset + 64));
165
+ const value = [
166
+ metaView.getFloat32(offset + 64, true),
167
+ metaView.getFloat32(offset + 68, true),
168
+ metaView.getFloat32(offset + 72, true),
169
+ metaView.getFloat32(offset + 76, true)
170
+ ];
171
+ const textureId = this.readString(sourceBlob.slice(offset + 80, offset + 144));
172
+ matData.properties.push({
173
+ fieldName,
174
+ value,
175
+ textureId
176
+ });
177
+ console.log(` Property "${fieldName}": value=[${value.join(', ')}], textureId="${textureId}"`);
178
+ offset += 144; // Size of MaterialPropertyValue
179
+ }
180
+ // Create Three.js material based on properties
181
+ const mat = new THREE__namespace.MeshStandardMaterial({
182
+ name: matData.name,
183
+ side: THREE__namespace.DoubleSide,
184
+ // Start with default gray color
185
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8)
186
+ });
187
+ console.log(`Creating material ${i}: "${matData.name}", ${matData.propertyCount} properties`);
188
+ // Apply properties
189
+ for (const prop of matData.properties) {
190
+ if (prop.fieldName.toLowerCase() === 'tint' || prop.fieldName.toLowerCase().includes('color')) {
191
+ // Apply color/tint
192
+ mat.color = new THREE__namespace.Color(prop.value[0], prop.value[1], prop.value[2]);
193
+ console.log(` Applied tint color: rgb(${prop.value[0]}, ${prop.value[1]}, ${prop.value[2]})`);
194
+ }
195
+ }
196
+ console.log(` Material ${i} final color: rgb(${mat.color.r}, ${mat.color.g}, ${mat.color.b})`);
197
+ materials.push(mat);
198
+ }
199
+ // Default material if none exist
200
+ if (materials.length === 0) {
201
+ materials.push(new THREE__namespace.MeshStandardMaterial({
202
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8), // Light gray
203
+ roughness: 0.5,
204
+ metalness: 0.0,
205
+ side: THREE__namespace.DoubleSide
206
+ }));
207
+ }
208
+ return { metadata, geometries, materials, materialData, nodes, meshIndices };
209
+ }
210
+ /**
211
+ * Create Three.js BufferGeometry from mesh data
212
+ */
213
+ static createGeometry(geoInfo, dataBlob) {
214
+ console.log('Creating geometry:', {
215
+ vertexCount: geoInfo.vertexCount,
216
+ indexCount: geoInfo.indexCount,
217
+ vertexOffset: geoInfo.vertexBufferOffset,
218
+ vertexSize: geoInfo.vertexBufferSize,
219
+ dataBlobSize: dataBlob.length
220
+ });
221
+ const geometry = new THREE__namespace.BufferGeometry();
222
+ const dataView = new DataView(dataBlob.buffer, dataBlob.byteOffset, dataBlob.byteLength);
223
+ // Extract vertex positions (3 floats per vertex)
224
+ if (geoInfo.vertexBufferSize > 0 && geoInfo.vertexBufferOffset + geoInfo.vertexBufferSize <= dataBlob.length) {
225
+ const positions = new Float32Array(geoInfo.vertexCount * 3);
226
+ for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
227
+ positions[i] = dataView.getFloat32(geoInfo.vertexBufferOffset + i * 4, true);
228
+ }
229
+ geometry.setAttribute('position', new THREE__namespace.BufferAttribute(positions, 3));
230
+ console.log('Added positions:', positions.slice(0, 9)); // First 3 vertices
231
+ }
232
+ else {
233
+ console.warn('No vertex data or out of bounds');
234
+ }
235
+ // Extract normals
236
+ if (geoInfo.normalBufferSize > 0 && geoInfo.normalBufferOffset + geoInfo.normalBufferSize <= dataBlob.length) {
237
+ const normals = new Float32Array(geoInfo.vertexCount * 3);
238
+ for (let i = 0; i < geoInfo.vertexCount * 3; i++) {
239
+ normals[i] = dataView.getFloat32(geoInfo.normalBufferOffset + i * 4, true);
240
+ }
241
+ geometry.setAttribute('normal', new THREE__namespace.BufferAttribute(normals, 3));
242
+ }
243
+ else {
244
+ // Compute normals if not provided
245
+ geometry.computeVertexNormals();
246
+ }
247
+ // Extract UVs
248
+ if (geoInfo.uvBufferSize > 0 && geoInfo.uvBufferOffset + geoInfo.uvBufferSize <= dataBlob.length) {
249
+ const uvs = new Float32Array(geoInfo.vertexCount * 2);
250
+ for (let i = 0; i < geoInfo.vertexCount * 2; i += 2) {
251
+ uvs[i] = dataView.getFloat32(geoInfo.uvBufferOffset + i * 4, true); // U
252
+ uvs[i + 1] = 1.0 - dataView.getFloat32(geoInfo.uvBufferOffset + (i + 1) * 4, true); // V (flipped)
253
+ }
254
+ geometry.setAttribute('uv', new THREE__namespace.BufferAttribute(uvs, 2));
255
+ }
256
+ // Extract indices
257
+ if (geoInfo.indexCount > 0 && geoInfo.indexBufferOffset + geoInfo.indexBufferSize <= dataBlob.length) {
258
+ const indices = new Uint32Array(geoInfo.indexCount);
259
+ // Read the first index to determine the base offset
260
+ let minIndex = Number.MAX_SAFE_INTEGER;
261
+ for (let i = 0; i < geoInfo.indexCount; i++) {
262
+ const index = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
263
+ if (index < minIndex)
264
+ minIndex = index;
265
+ }
266
+ // Read indices again and offset them to start from 0
267
+ for (let i = 0; i < geoInfo.indexCount; i++) {
268
+ const absoluteIndex = dataView.getUint32(geoInfo.indexBufferOffset + i * 4, true);
269
+ indices[i] = absoluteIndex - minIndex;
270
+ }
271
+ geometry.setIndex(new THREE__namespace.BufferAttribute(indices, 1));
272
+ console.log(`Added indices (offset by ${minIndex}):`, indices.slice(0, 12)); // First 4 triangles
273
+ }
274
+ else if (geoInfo.indexCount > 0) {
275
+ console.warn('Index data out of bounds');
276
+ }
277
+ // Ensure we have normals (compute if missing)
278
+ if (!geometry.attributes.normal) {
279
+ geometry.computeVertexNormals();
280
+ }
281
+ geometry.computeBoundingSphere();
282
+ geometry.computeBoundingBox();
283
+ console.log('Final geometry:', {
284
+ hasPositions: !!geometry.attributes.position,
285
+ hasNormals: !!geometry.attributes.normal,
286
+ hasUVs: !!geometry.attributes.uv,
287
+ hasIndices: !!geometry.index,
288
+ vertexCount: geometry.attributes.position?.count || 0
289
+ });
290
+ return geometry;
291
+ }
292
+ /**
293
+ * Build Three.js scene from parsed mesh data
294
+ */
295
+ static buildScene(parsedData, dataBlob) {
296
+ const root = new THREE__namespace.Group();
297
+ root.name = 'StowKitMesh';
298
+ const { geometries, materials, nodes, meshIndices } = parsedData;
299
+ console.log('Building scene with:', {
300
+ geometries: geometries.length,
301
+ materials: materials.length,
302
+ nodes: nodes.length,
303
+ meshIndices: meshIndices.length
304
+ });
305
+ // Create all Three.js objects for nodes
306
+ const nodeObjects = [];
307
+ for (const node of nodes) {
308
+ const obj = new THREE__namespace.Group();
309
+ obj.name = node.name;
310
+ console.log(`Node ${node.name}: position=${node.position}, rotation=${node.rotation}, scale=${node.scale}, meshCount=${node.meshCount}, firstMeshIndex=${node.firstMeshIndex}`);
311
+ // Apply transform
312
+ obj.position.set(...node.position);
313
+ obj.quaternion.set(...node.rotation);
314
+ obj.scale.set(...node.scale);
315
+ // Add meshes to this node
316
+ for (let i = 0; i < node.meshCount; i++) {
317
+ const meshIndexArrayPos = node.firstMeshIndex + i;
318
+ const meshIndex = meshIndices[meshIndexArrayPos];
319
+ console.log(` meshIndices[${meshIndexArrayPos}] = ${meshIndex} (adding to node ${node.name})`);
320
+ if (meshIndex < geometries.length) {
321
+ const geoInfo = geometries[meshIndex];
322
+ console.log(` Creating geometry from geoInfo: vertices=${geoInfo.vertexCount}, indices=${geoInfo.indexCount}, material=${geoInfo.materialIndex}`);
323
+ const geometry = this.createGeometry(geoInfo, dataBlob);
324
+ // Use assigned material if valid, otherwise create default
325
+ let material;
326
+ if (geoInfo.materialIndex < materials.length) {
327
+ material = materials[geoInfo.materialIndex];
328
+ }
329
+ else {
330
+ console.warn(` Material index ${geoInfo.materialIndex} out of bounds, using default`);
331
+ material = new THREE__namespace.MeshStandardMaterial({
332
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8),
333
+ roughness: 0.5,
334
+ metalness: 0.0,
335
+ side: THREE__namespace.DoubleSide
336
+ });
337
+ }
338
+ const mesh = new THREE__namespace.Mesh(geometry, material);
339
+ obj.add(mesh);
340
+ console.log(` ✓ Added mesh to node ${node.name} (${geometry.attributes.position?.count || 0} vertices, material: ${material.name || 'default'})`);
341
+ }
342
+ else {
343
+ console.error(` ✗ Mesh index ${meshIndex} out of bounds (have ${geometries.length} geometries)`);
344
+ }
345
+ }
346
+ console.log(` Node ${node.name} now has ${obj.children.length} children`);
347
+ nodeObjects.push(obj);
348
+ }
349
+ // Build hierarchy
350
+ console.log('Building node hierarchy...');
351
+ for (let i = 0; i < nodes.length; i++) {
352
+ const node = nodes[i];
353
+ const obj = nodeObjects[i];
354
+ if (node.parentIndex >= 0 && node.parentIndex < nodeObjects.length) {
355
+ console.log(` Adding node "${node.name}" (index ${i}) as child of node "${nodes[node.parentIndex].name}" (index ${node.parentIndex})`);
356
+ nodeObjects[node.parentIndex].add(obj);
357
+ }
358
+ else {
359
+ console.log(` Adding node "${node.name}" (index ${i}) as direct child of root (parentIndex=${node.parentIndex})`);
360
+ root.add(obj);
361
+ }
362
+ }
363
+ // If no nodes, create a single mesh from all geometries
364
+ if (nodes.length === 0 && geometries.length > 0) {
365
+ console.log('No nodes found, creating meshes directly from geometries');
366
+ geometries.forEach((geoInfo, index) => {
367
+ console.log(`Creating direct mesh ${index}: materialIndex=${geoInfo.materialIndex}, vertices=${geoInfo.vertexCount}`);
368
+ const geometry = this.createGeometry(geoInfo, dataBlob);
369
+ // Use assigned material if valid, otherwise create default
370
+ let material;
371
+ if (geoInfo.materialIndex < materials.length) {
372
+ material = materials[geoInfo.materialIndex];
373
+ }
374
+ else {
375
+ console.warn(` Material index ${geoInfo.materialIndex} out of bounds, using default`);
376
+ material = new THREE__namespace.MeshStandardMaterial({
377
+ color: new THREE__namespace.Color(0.8, 0.8, 0.8),
378
+ roughness: 0.5,
379
+ metalness: 0.0,
380
+ side: THREE__namespace.DoubleSide
381
+ });
382
+ }
383
+ const mesh = new THREE__namespace.Mesh(geometry, material);
384
+ mesh.name = `Mesh_${index}`;
385
+ root.add(mesh);
386
+ console.log(`Added mesh ${index} to root`);
387
+ });
388
+ }
389
+ console.log(`Final scene has ${root.children.length} direct children`);
390
+ // Debug: traverse and count all meshes, showing full hierarchy
391
+ let totalMeshCount = 0;
392
+ let hierarchyLog = 'Scene hierarchy:\n';
393
+ root.traverse((obj) => {
394
+ // Calculate depth by counting parents
395
+ let depth = 0;
396
+ let parent = obj.parent;
397
+ while (parent && parent !== root) {
398
+ depth++;
399
+ parent = parent.parent;
400
+ }
401
+ const indent = ' '.repeat(depth);
402
+ if (obj instanceof THREE__namespace.Mesh) {
403
+ totalMeshCount++;
404
+ const mat = obj.material;
405
+ hierarchyLog += `${indent}[Mesh] vertices=${obj.geometry.attributes.position?.count || 0}, material=${mat.name}, hasTexture=${!!mat.map}\n`;
406
+ }
407
+ else if (obj instanceof THREE__namespace.Group && obj !== root) {
408
+ hierarchyLog += `${indent}[Group] ${obj.name} (${obj.children.length} children)\n`;
409
+ }
410
+ });
411
+ console.log(hierarchyLog);
412
+ console.log(`Total meshes in scene: ${totalMeshCount}`);
413
+ return root;
414
+ }
415
+ /**
416
+ * Read null-terminated string from buffer
417
+ */
418
+ static readString(buffer) {
419
+ let end = buffer.indexOf(0);
420
+ if (end === -1)
421
+ end = buffer.length;
422
+ return new TextDecoder().decode(buffer.slice(0, end));
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Three.js loader for StowKit asset packs (.stow files)
428
+ *
429
+ * Usage:
430
+ * ```typescript
431
+ * const loader = new StowKitLoader();
432
+ * loader.setTranscoderPath('/basis/');
433
+ *
434
+ * // Load a mesh asset
435
+ * loader.loadMesh('assets.stow', 'path/to/mesh', (scene) => {
436
+ * threeScene.add(scene);
437
+ * });
438
+ * ```
439
+ */
440
+ class StowKitLoader extends THREE__namespace.Loader {
441
+ constructor(manager, parameters) {
442
+ super(manager);
443
+ this.ownedReader = false;
444
+ // Use provided reader or create new one
445
+ if (parameters?.reader) {
446
+ this.reader = parameters.reader;
447
+ this.ownedReader = false;
448
+ }
449
+ else {
450
+ const wasmPath = parameters?.wasmPath || '/stowkit_reader.wasm';
451
+ this.reader = new reader.StowKitReader(wasmPath);
452
+ this.ownedReader = true;
453
+ }
454
+ // Setup KTX2 loader for textures with default path
455
+ const transcoderPath = parameters?.transcoderPath || '/basis/';
456
+ this.ktx2Loader = new KTX2Loader_js.KTX2Loader(manager);
457
+ this.ktx2Loader.setTranscoderPath(transcoderPath);
458
+ // Detect support with a temporary renderer
459
+ // This is required for KTX2Loader to work
460
+ const tempRenderer = new THREE__namespace.WebGLRenderer();
461
+ this.ktx2Loader.detectSupport(tempRenderer);
462
+ tempRenderer.dispose();
463
+ }
464
+ /**
465
+ * Set the path to the Basis Universal transcoder
466
+ */
467
+ setTranscoderPath(path) {
468
+ this.ktx2Loader.setTranscoderPath(path);
469
+ return this;
470
+ }
471
+ /**
472
+ * Detect WebGL support for compressed textures
473
+ */
474
+ detectSupport(renderer) {
475
+ this.ktx2Loader.detectSupport(renderer);
476
+ return this;
477
+ }
478
+ /**
479
+ * Open a .stow pack file (call this first if loading multiple assets)
480
+ * @param url - URL to the .stow file
481
+ */
482
+ async openPack(url) {
483
+ if (this.ownedReader) {
484
+ await this.reader.init();
485
+ }
486
+ const response = await fetch(url);
487
+ if (!response.ok) {
488
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
489
+ }
490
+ const arrayBuffer = await response.arrayBuffer();
491
+ await this.reader.open(arrayBuffer);
492
+ }
493
+ /**
494
+ * Load and parse a mesh asset from a StowKit pack
495
+ * @param url - URL to the .stow file (or omit if already opened with openPack)
496
+ * @param assetPath - Path to the mesh asset within the pack
497
+ * @param onLoad - Callback when loading completes
498
+ * @param onProgress - Progress callback
499
+ * @param onError - Error callback
500
+ */
501
+ async loadMesh(url, assetPath, onLoad, onProgress, onError) {
502
+ try {
503
+ // Open pack if URL provided
504
+ if (url) {
505
+ await this.openPack(url);
506
+ }
507
+ // Find the mesh asset
508
+ const assetIndex = this.reader.findAssetByPath(assetPath);
509
+ if (assetIndex < 0) {
510
+ throw new Error(`Asset not found: ${assetPath}`);
511
+ }
512
+ // Read mesh data and metadata
513
+ const data = this.reader.readAssetData(assetIndex);
514
+ const metadata = this.reader.readAssetMetadata(assetIndex);
515
+ if (!data) {
516
+ throw new Error(`Failed to read mesh data for ${assetPath}`);
517
+ }
518
+ if (!metadata) {
519
+ throw new Error(`No metadata available for ${assetPath}`);
520
+ }
521
+ // Parse mesh data
522
+ const parsedData = MeshParser.parseMeshData(metadata, data);
523
+ // Load textures for materials
524
+ await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
525
+ // Build Three.js scene
526
+ const scene = MeshParser.buildScene(parsedData, data);
527
+ if (onLoad) {
528
+ onLoad(scene);
529
+ }
530
+ return scene;
531
+ }
532
+ catch (error) {
533
+ if (onError) {
534
+ onError(error);
535
+ }
536
+ throw error;
537
+ }
538
+ }
539
+ /**
540
+ * Load textures for materials
541
+ */
542
+ async loadMaterialTextures(materialData, materials) {
543
+ for (let i = 0; i < materialData.length; i++) {
544
+ const matData = materialData[i];
545
+ const material = materials[i];
546
+ for (const prop of matData.properties) {
547
+ if (prop.textureId && prop.textureId.length > 0) {
548
+ try {
549
+ // Find texture asset
550
+ const textureIndex = this.reader.findAssetByPath(prop.textureId);
551
+ if (textureIndex < 0) {
552
+ console.warn(`Texture not found: ${prop.textureId}`);
553
+ continue;
554
+ }
555
+ // Read texture data
556
+ const textureData = this.reader.readAssetData(textureIndex);
557
+ if (!textureData) {
558
+ console.warn(`Failed to read texture: ${prop.textureId}`);
559
+ continue;
560
+ }
561
+ // Load as KTX2
562
+ const texture = await this.loadKTX2Texture(textureData);
563
+ // Apply to material based on property name
564
+ this.applyTextureToMaterial(material, prop.fieldName, texture);
565
+ }
566
+ catch (error) {
567
+ console.error(`Failed to load texture "${prop.textureId}":`, error);
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ /**
574
+ * Load a KTX2 texture from binary data
575
+ */
576
+ async loadKTX2Texture(data) {
577
+ const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
578
+ const blob = new Blob([arrayBuffer]);
579
+ const url = URL.createObjectURL(blob);
580
+ try {
581
+ return await new Promise((resolve, reject) => {
582
+ this.ktx2Loader.load(url, (texture) => {
583
+ URL.revokeObjectURL(url);
584
+ // Compressed textures don't support flipY, we flip UVs in MeshParser instead
585
+ texture.needsUpdate = true;
586
+ resolve(texture);
587
+ }, undefined, (error) => {
588
+ URL.revokeObjectURL(url);
589
+ reject(error);
590
+ });
591
+ });
592
+ }
593
+ catch (error) {
594
+ URL.revokeObjectURL(url);
595
+ throw error;
596
+ }
597
+ }
598
+ /**
599
+ * Apply texture to appropriate material property
600
+ */
601
+ applyTextureToMaterial(material, propertyName, texture) {
602
+ const propNameLower = propertyName.toLowerCase();
603
+ if (propNameLower === 'maintex' || propNameLower.includes('diffuse') ||
604
+ propNameLower.includes('albedo') || propNameLower.includes('base')) {
605
+ material.map = texture;
606
+ }
607
+ else if (propNameLower.includes('normal')) {
608
+ material.normalMap = texture;
609
+ }
610
+ else if (propNameLower.includes('metallic')) {
611
+ material.metalnessMap = texture;
612
+ }
613
+ else if (propNameLower.includes('roughness')) {
614
+ material.roughnessMap = texture;
615
+ }
616
+ else {
617
+ // Default to main texture
618
+ material.map = texture;
619
+ }
620
+ material.needsUpdate = true;
621
+ }
622
+ /**
623
+ * Load a texture asset from a StowKit pack
624
+ * @param url - URL to the .stow file (or omit if already opened with openPack)
625
+ * @param assetPath - Path to the texture asset within the pack
626
+ * @param onLoad - Callback when loading completes
627
+ * @param onProgress - Progress callback
628
+ * @param onError - Error callback
629
+ */
630
+ async loadTexture(url, assetPath, onLoad, onProgress, onError) {
631
+ try {
632
+ // Open pack if URL provided
633
+ if (url) {
634
+ await this.openPack(url);
635
+ }
636
+ // Find the texture asset
637
+ const assetIndex = this.reader.findAssetByPath(assetPath);
638
+ if (assetIndex < 0) {
639
+ throw new Error(`Asset not found: ${assetPath}`);
640
+ }
641
+ return await this.loadTextureByIndex(assetIndex, onLoad, onProgress, onError);
642
+ }
643
+ catch (error) {
644
+ if (onError) {
645
+ onError(error);
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ /**
651
+ * Load a texture asset by its index in the pack
652
+ * @param index - Asset index
653
+ * @param onLoad - Callback when loading completes
654
+ * @param onProgress - Progress callback
655
+ * @param onError - Error callback
656
+ */
657
+ async loadTextureByIndex(index, onLoad, onProgress, onError) {
658
+ try {
659
+ // Read texture data
660
+ const data = this.reader.readAssetData(index);
661
+ if (!data) {
662
+ throw new Error(`Failed to read texture data for index ${index}`);
663
+ }
664
+ // Load as KTX2
665
+ const texture = await this.loadKTX2Texture(data);
666
+ if (onLoad) {
667
+ onLoad(texture);
668
+ }
669
+ return texture;
670
+ }
671
+ catch (error) {
672
+ if (onError) {
673
+ onError(error);
674
+ }
675
+ throw error;
676
+ }
677
+ }
678
+ /**
679
+ * Load an audio asset from a StowKit pack
680
+ * @param url - URL to the .stow file (or omit if already opened with openPack)
681
+ * @param assetPath - Path to the audio asset within the pack
682
+ * @param listener - THREE.AudioListener to attach to
683
+ * @param onLoad - Callback when loading completes
684
+ * @param onProgress - Progress callback
685
+ * @param onError - Error callback
686
+ */
687
+ async loadAudio(url, assetPath, listener, onLoad, onProgress, onError) {
688
+ try {
689
+ // Open pack if URL provided
690
+ if (url) {
691
+ await this.openPack(url);
692
+ }
693
+ // Find the audio asset
694
+ const assetIndex = this.reader.findAssetByPath(assetPath);
695
+ if (assetIndex < 0) {
696
+ throw new Error(`Asset not found: ${assetPath}`);
697
+ }
698
+ // Read audio data
699
+ const data = this.reader.readAssetData(assetIndex);
700
+ if (!data) {
701
+ throw new Error(`Failed to read audio data for ${assetPath}`);
702
+ }
703
+ // Create audio object
704
+ const audio = new THREE__namespace.Audio(listener);
705
+ // Decode audio data
706
+ const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
707
+ const audioContext = listener.context;
708
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
709
+ audio.setBuffer(audioBuffer);
710
+ if (onLoad) {
711
+ onLoad(audio);
712
+ }
713
+ return audio;
714
+ }
715
+ catch (error) {
716
+ if (onError) {
717
+ onError(error);
718
+ }
719
+ throw error;
720
+ }
721
+ }
722
+ /**
723
+ * Get metadata for an audio asset
724
+ * @param assetPath - Path to the audio asset within the pack
725
+ */
726
+ getAudioMetadata(assetPath) {
727
+ try {
728
+ const assetIndex = this.reader.findAssetByPath(assetPath);
729
+ if (assetIndex < 0)
730
+ return null;
731
+ const metadata = this.reader.readAssetMetadata(assetIndex);
732
+ if (!metadata || metadata.length < 140)
733
+ return null;
734
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
735
+ const decoder = new TextDecoder();
736
+ // Parse AudioMetadata structure
737
+ const stringIdBytes = metadata.slice(0, 128);
738
+ const nullIdx = stringIdBytes.indexOf(0);
739
+ const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
740
+ return {
741
+ stringId,
742
+ sampleRate: view.getUint32(128, true),
743
+ channels: view.getUint32(132, true),
744
+ durationMs: view.getUint32(136, true)
745
+ };
746
+ }
747
+ catch (error) {
748
+ console.error('Failed to read audio metadata:', error);
749
+ return null;
750
+ }
751
+ }
752
+ /**
753
+ * Get metadata for a texture asset
754
+ * @param assetPath - Path to the texture asset within the pack
755
+ */
756
+ getTextureMetadata(assetPath) {
757
+ try {
758
+ const assetIndex = this.reader.findAssetByPath(assetPath);
759
+ if (assetIndex < 0)
760
+ return null;
761
+ const metadata = this.reader.readAssetMetadata(assetIndex);
762
+ if (!metadata || metadata.length < 148)
763
+ return null;
764
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
765
+ const decoder = new TextDecoder();
766
+ // Parse TextureMetadata structure
767
+ const stringIdBytes = metadata.slice(20, 148);
768
+ const nullIdx = stringIdBytes.indexOf(0);
769
+ const stringId = decoder.decode(stringIdBytes.slice(0, nullIdx >= 0 ? nullIdx : 128));
770
+ return {
771
+ width: view.getUint32(0, true),
772
+ height: view.getUint32(4, true),
773
+ channels: view.getUint32(8, true),
774
+ isKtx2: view.getUint32(12, true) === 1,
775
+ channelFormat: view.getUint32(16, true),
776
+ stringId
777
+ };
778
+ }
779
+ catch (error) {
780
+ console.error('Failed to read texture metadata:', error);
781
+ return null;
782
+ }
783
+ }
784
+ /**
785
+ * Load material schema information by index
786
+ * @param index - Asset index
787
+ */
788
+ loadMaterialSchemaByIndex(index) {
789
+ try {
790
+ const metadata = this.reader.readAssetMetadata(index);
791
+ if (!metadata || metadata.length < 196)
792
+ return null;
793
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
794
+ const decoder = new TextDecoder();
795
+ const stringIdBytes = metadata.slice(0, 128);
796
+ const schemaNameBytes = metadata.slice(128, 192);
797
+ const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
798
+ const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
799
+ const fieldCount = view.getUint32(192, true);
800
+ const fields = [];
801
+ let offset = 196;
802
+ for (let i = 0; i < fieldCount; i++) {
803
+ if (offset + 88 > metadata.length)
804
+ break;
805
+ const fieldNameBytes = metadata.slice(offset, offset + 64);
806
+ const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
807
+ const fieldType = view.getUint32(offset + 64, true);
808
+ const previewFlags = view.getUint32(offset + 68, true);
809
+ const defaultValue = [
810
+ view.getFloat32(offset + 72, true),
811
+ view.getFloat32(offset + 76, true),
812
+ view.getFloat32(offset + 80, true),
813
+ view.getFloat32(offset + 84, true)
814
+ ];
815
+ const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
816
+ const previewFlagNames = ['None', 'MainTex', 'Tint'];
817
+ fields.push({
818
+ name: fieldName,
819
+ type: typeNames[fieldType] || 'Float',
820
+ previewFlag: previewFlagNames[previewFlags] || 'None',
821
+ defaultValue
822
+ });
823
+ offset += 88;
824
+ }
825
+ return { stringId, schemaName, fields };
826
+ }
827
+ catch (error) {
828
+ console.error('Failed to read material schema:', error);
829
+ return null;
830
+ }
831
+ }
832
+ /**
833
+ * Load material schema information by path
834
+ * @param assetPath - Path to the material schema asset
835
+ */
836
+ loadMaterialSchema(assetPath) {
837
+ try {
838
+ const assetIndex = this.reader.findAssetByPath(assetPath);
839
+ if (assetIndex < 0)
840
+ return null;
841
+ const metadata = this.reader.readAssetMetadata(assetIndex);
842
+ if (!metadata || metadata.length < 196)
843
+ return null;
844
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
845
+ const decoder = new TextDecoder();
846
+ // Parse MaterialSchemaMetadata: string_id[128] + schema_name[64] + field_count(4) = 196 bytes
847
+ const stringIdBytes = metadata.slice(0, 128);
848
+ const schemaNameBytes = metadata.slice(128, 192);
849
+ const stringId = decoder.decode(stringIdBytes.slice(0, stringIdBytes.indexOf(0) || 128));
850
+ const schemaName = decoder.decode(schemaNameBytes.slice(0, schemaNameBytes.indexOf(0) || 64));
851
+ const fieldCount = view.getUint32(192, true);
852
+ const fields = [];
853
+ let offset = 196;
854
+ // Parse fields: name[64] + field_type(4) + preview_flags(4) + default_value[4*4=16] = 88 bytes
855
+ for (let i = 0; i < fieldCount; i++) {
856
+ if (offset + 88 > metadata.length)
857
+ break;
858
+ const fieldNameBytes = metadata.slice(offset, offset + 64);
859
+ const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
860
+ const fieldType = view.getUint32(offset + 64, true);
861
+ const previewFlags = view.getUint32(offset + 68, true);
862
+ const defaultValue = [
863
+ view.getFloat32(offset + 72, true),
864
+ view.getFloat32(offset + 76, true),
865
+ view.getFloat32(offset + 80, true),
866
+ view.getFloat32(offset + 84, true)
867
+ ];
868
+ const typeNames = ['Texture', 'Color', 'Float', 'Vec2', 'Vec3', 'Vec4', 'Int'];
869
+ const previewFlagNames = ['None', 'MainTex', 'Tint'];
870
+ fields.push({
871
+ name: fieldName,
872
+ type: typeNames[fieldType] || 'Float',
873
+ previewFlag: previewFlagNames[previewFlags] || 'None',
874
+ defaultValue
875
+ });
876
+ offset += 88; // MaterialSchemaField is 88 bytes total
877
+ }
878
+ return { stringId, schemaName, fields };
879
+ }
880
+ catch (error) {
881
+ console.error('Failed to read material schema:', error);
882
+ return null;
883
+ }
884
+ }
885
+ /**
886
+ * Get material information from a loaded mesh
887
+ * Returns material data including properties and texture references
888
+ */
889
+ getMeshMaterials(assetPath) {
890
+ try {
891
+ const assetIndex = this.reader.findAssetByPath(assetPath);
892
+ if (assetIndex < 0)
893
+ return null;
894
+ const metadata = this.reader.readAssetMetadata(assetIndex);
895
+ if (!metadata || metadata.length < 140)
896
+ return null;
897
+ const view = new DataView(metadata.buffer, metadata.byteOffset, metadata.byteLength);
898
+ const decoder = new TextDecoder();
899
+ // Parse MeshMetadata
900
+ const meshGeometryCount = view.getUint32(0, true);
901
+ const materialCount = view.getUint32(4, true);
902
+ const nodeCount = view.getUint32(8, true);
903
+ let offset = 140;
904
+ // Skip geometries (76 bytes each)
905
+ offset += meshGeometryCount * 76;
906
+ // Parse materials
907
+ const materials = [];
908
+ for (let i = 0; i < materialCount; i++) {
909
+ if (offset + 196 > metadata.length)
910
+ break;
911
+ const nameBytes = metadata.slice(offset, offset + 64);
912
+ const schemaIdBytes = metadata.slice(offset + 64, offset + 192);
913
+ const name = decoder.decode(nameBytes.slice(0, nameBytes.indexOf(0) || 64));
914
+ const schemaId = decoder.decode(schemaIdBytes.slice(0, schemaIdBytes.indexOf(0) || 128));
915
+ const propertyCount = view.getUint32(offset + 192, true);
916
+ materials.push({ name, schemaId, propertyCount, properties: [] });
917
+ offset += 196;
918
+ }
919
+ // Skip nodes (116 bytes each)
920
+ offset += nodeCount * 116;
921
+ // Skip mesh indices (4 bytes each)
922
+ const totalMeshRefs = materials.reduce((sum, mat) => {
923
+ // This is a simplification - we'd need to parse nodes to get exact count
924
+ return sum;
925
+ }, 0);
926
+ // For now, skip based on nodeCount assumption
927
+ offset += nodeCount * 4;
928
+ // Parse material properties
929
+ for (const mat of materials) {
930
+ for (let j = 0; j < mat.propertyCount; j++) {
931
+ if (offset + 144 > metadata.length)
932
+ break;
933
+ const fieldNameBytes = metadata.slice(offset, offset + 64);
934
+ const fieldName = decoder.decode(fieldNameBytes.slice(0, fieldNameBytes.indexOf(0) || 64));
935
+ const value = [
936
+ view.getFloat32(offset + 64, true),
937
+ view.getFloat32(offset + 68, true),
938
+ view.getFloat32(offset + 72, true),
939
+ view.getFloat32(offset + 76, true)
940
+ ];
941
+ const textureIdBytes = metadata.slice(offset + 80, offset + 144);
942
+ const textureId = decoder.decode(textureIdBytes.slice(0, textureIdBytes.indexOf(0) || 64));
943
+ mat.properties.push({ fieldName, value, textureId });
944
+ offset += 144;
945
+ }
946
+ }
947
+ return materials;
948
+ }
949
+ catch (error) {
950
+ console.error('Failed to read mesh materials:', error);
951
+ return null;
952
+ }
953
+ }
954
+ /**
955
+ * Open a .stow file from a File or ArrayBuffer
956
+ */
957
+ async open(fileOrBuffer) {
958
+ // Initialize reader if needed
959
+ if (this.ownedReader) {
960
+ await this.reader.init();
961
+ }
962
+ return await this.reader.open(fileOrBuffer);
963
+ }
964
+ /**
965
+ * Get the total number of assets
966
+ */
967
+ getAssetCount() {
968
+ return this.reader.getAssetCount();
969
+ }
970
+ /**
971
+ * Get info about a specific asset
972
+ */
973
+ getAssetInfo(index) {
974
+ return this.reader.getAssetInfo(index);
975
+ }
976
+ /**
977
+ * Read asset data by index
978
+ */
979
+ readAssetData(index) {
980
+ return this.reader.readAssetData(index);
981
+ }
982
+ /**
983
+ * Read asset metadata by index
984
+ */
985
+ readAssetMetadata(index) {
986
+ return this.reader.readAssetMetadata(index);
987
+ }
988
+ /**
989
+ * Parse texture metadata
990
+ */
991
+ parseTextureMetadata(metadataBytes) {
992
+ return this.reader.parseTextureMetadata(metadataBytes);
993
+ }
994
+ /**
995
+ * Create an HTML audio element for preview
996
+ * @param index - Asset index
997
+ */
998
+ async createAudioPreview(index) {
999
+ const data = this.reader.readAssetData(index);
1000
+ if (!data) {
1001
+ throw new Error(`Failed to read audio data for index ${index}`);
1002
+ }
1003
+ // Create blob URL for HTML5 audio
1004
+ const blob = new Blob([data], { type: 'audio/ogg' });
1005
+ const url = URL.createObjectURL(blob);
1006
+ // Create and configure audio element
1007
+ const audio = document.createElement('audio');
1008
+ audio.controls = true;
1009
+ audio.src = url;
1010
+ // Clean up URL when audio ends or on error
1011
+ audio.addEventListener('ended', () => URL.revokeObjectURL(url));
1012
+ audio.addEventListener('error', () => URL.revokeObjectURL(url));
1013
+ return audio;
1014
+ }
1015
+ /**
1016
+ * List all assets in the opened pack
1017
+ */
1018
+ listAssets() {
1019
+ return this.reader.listAssets();
1020
+ }
1021
+ /**
1022
+ * Dispose of resources
1023
+ */
1024
+ dispose() {
1025
+ this.ktx2Loader.dispose();
1026
+ if (this.ownedReader) {
1027
+ this.reader.close();
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ exports.MeshParser = MeshParser;
1033
+ exports.StowKitLoader = StowKitLoader;
1034
+ //# sourceMappingURL=stowkit-three-loader.js.map