@stowkit/three-loader 0.1.30 → 0.1.32
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/AssetCache.d.ts +54 -0
- package/dist/AssetCache.d.ts.map +1 -0
- package/dist/MeshParser.d.ts +2 -2
- package/dist/MeshParser.d.ts.map +1 -1
- package/dist/StowKitLoader.d.ts +4 -1
- package/dist/StowKitLoader.d.ts.map +1 -1
- package/dist/StowKitPack.d.ts +3 -1
- package/dist/StowKitPack.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/stowkit-three-loader.esm.js +235 -22
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +235 -21
- package/dist/stowkit-three-loader.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,6 +24,154 @@ function _interopNamespaceDefault(e) {
|
|
|
24
24
|
|
|
25
25
|
var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Persistent asset cache using IndexedDB
|
|
29
|
+
* Caches decoded Draco geometries and transcoded Basis textures
|
|
30
|
+
* Invalidates when source .stow file changes
|
|
31
|
+
*/
|
|
32
|
+
class AssetMemoryCache {
|
|
33
|
+
static async init() {
|
|
34
|
+
if (this.db)
|
|
35
|
+
return;
|
|
36
|
+
if (this.initPromise)
|
|
37
|
+
return this.initPromise;
|
|
38
|
+
this.initPromise = new Promise((resolve, reject) => {
|
|
39
|
+
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
|
40
|
+
request.onerror = () => reject(request.error);
|
|
41
|
+
request.onsuccess = () => {
|
|
42
|
+
this.db = request.result;
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
request.onupgradeneeded = (event) => {
|
|
46
|
+
const db = event.target.result;
|
|
47
|
+
if (!db.objectStoreNames.contains(this.GEOMETRY_STORE)) {
|
|
48
|
+
db.createObjectStore(this.GEOMETRY_STORE);
|
|
49
|
+
}
|
|
50
|
+
if (!db.objectStoreNames.contains(this.TEXTURE_STORE)) {
|
|
51
|
+
db.createObjectStore(this.TEXTURE_STORE);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return this.initPromise;
|
|
56
|
+
}
|
|
57
|
+
static async getGeometry(key, version) {
|
|
58
|
+
try {
|
|
59
|
+
await this.init();
|
|
60
|
+
if (!this.db)
|
|
61
|
+
return null;
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const transaction = this.db.transaction([this.GEOMETRY_STORE], 'readonly');
|
|
64
|
+
const store = transaction.objectStore(this.GEOMETRY_STORE);
|
|
65
|
+
const request = store.get(key);
|
|
66
|
+
request.onsuccess = () => {
|
|
67
|
+
const cached = request.result;
|
|
68
|
+
if (cached && cached.version === version) {
|
|
69
|
+
resolve(cached);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resolve(null);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
request.onerror = () => resolve(null);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
static async setGeometry(key, version, geometry) {
|
|
83
|
+
try {
|
|
84
|
+
await this.init();
|
|
85
|
+
if (!this.db)
|
|
86
|
+
return;
|
|
87
|
+
const cached = { ...geometry, version };
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const transaction = this.db.transaction([this.GEOMETRY_STORE], 'readwrite');
|
|
90
|
+
const store = transaction.objectStore(this.GEOMETRY_STORE);
|
|
91
|
+
store.put(cached, key);
|
|
92
|
+
transaction.oncomplete = () => resolve();
|
|
93
|
+
transaction.onerror = () => resolve();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Silently fail if IndexedDB unavailable
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
static async getTexture(key, version) {
|
|
101
|
+
try {
|
|
102
|
+
await this.init();
|
|
103
|
+
if (!this.db)
|
|
104
|
+
return null;
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const transaction = this.db.transaction([this.TEXTURE_STORE], 'readonly');
|
|
107
|
+
const store = transaction.objectStore(this.TEXTURE_STORE);
|
|
108
|
+
const request = store.get(key);
|
|
109
|
+
request.onsuccess = () => {
|
|
110
|
+
const cached = request.result;
|
|
111
|
+
if (cached && cached.version === version) {
|
|
112
|
+
resolve(cached);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
resolve(null);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
request.onerror = () => resolve(null);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
static async setTexture(key, version, textureData) {
|
|
126
|
+
try {
|
|
127
|
+
await this.init();
|
|
128
|
+
if (!this.db)
|
|
129
|
+
return;
|
|
130
|
+
const cached = { ...textureData, version };
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const transaction = this.db.transaction([this.TEXTURE_STORE], 'readwrite');
|
|
133
|
+
const store = transaction.objectStore(this.TEXTURE_STORE);
|
|
134
|
+
store.put(cached, key);
|
|
135
|
+
transaction.oncomplete = () => resolve();
|
|
136
|
+
transaction.onerror = () => resolve();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Silently fail if IndexedDB unavailable
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
static async clear() {
|
|
144
|
+
try {
|
|
145
|
+
await this.init();
|
|
146
|
+
if (!this.db)
|
|
147
|
+
return;
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const transaction = this.db.transaction([this.GEOMETRY_STORE, this.TEXTURE_STORE], 'readwrite');
|
|
150
|
+
transaction.objectStore(this.GEOMETRY_STORE).clear();
|
|
151
|
+
transaction.objectStore(this.TEXTURE_STORE).clear();
|
|
152
|
+
transaction.oncomplete = () => resolve();
|
|
153
|
+
transaction.onerror = () => resolve();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Silently fail
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Generate a version hash for cache invalidation
|
|
162
|
+
* Based on file size only
|
|
163
|
+
*/
|
|
164
|
+
static generateVersion(fileSize) {
|
|
165
|
+
return `${fileSize}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
AssetMemoryCache.db = null;
|
|
169
|
+
AssetMemoryCache.initPromise = null;
|
|
170
|
+
AssetMemoryCache.DB_NAME = 'StowKitAssetCache';
|
|
171
|
+
AssetMemoryCache.DB_VERSION = 1;
|
|
172
|
+
AssetMemoryCache.GEOMETRY_STORE = 'geometries';
|
|
173
|
+
AssetMemoryCache.TEXTURE_STORE = 'textures';
|
|
174
|
+
|
|
27
175
|
class MeshParser {
|
|
28
176
|
/**
|
|
29
177
|
* Parse mesh metadata from binary data
|
|
@@ -211,7 +359,23 @@ class MeshParser {
|
|
|
211
359
|
/**
|
|
212
360
|
* Create Three.js BufferGeometry from Draco compressed mesh data
|
|
213
361
|
*/
|
|
214
|
-
static async createGeometry(geoInfo, dataBlob, dracoLoader) {
|
|
362
|
+
static async createGeometry(geoInfo, dataBlob, dracoLoader, cacheKey, version) {
|
|
363
|
+
// Try loading from IndexedDB cache first
|
|
364
|
+
if (cacheKey && version) {
|
|
365
|
+
const cached = await AssetMemoryCache.getGeometry(cacheKey, version);
|
|
366
|
+
if (cached) {
|
|
367
|
+
const geometry = new THREE__namespace.BufferGeometry();
|
|
368
|
+
geometry.setAttribute('position', new THREE__namespace.BufferAttribute(cached.positions, 3));
|
|
369
|
+
if (cached.normals) {
|
|
370
|
+
geometry.setAttribute('normal', new THREE__namespace.BufferAttribute(cached.normals, 3));
|
|
371
|
+
}
|
|
372
|
+
if (cached.uvs) {
|
|
373
|
+
geometry.setAttribute('uv', new THREE__namespace.BufferAttribute(cached.uvs, 2));
|
|
374
|
+
}
|
|
375
|
+
geometry.setIndex(new THREE__namespace.BufferAttribute(cached.indices, 1));
|
|
376
|
+
return geometry;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
215
379
|
// Extract the Draco compressed buffer
|
|
216
380
|
if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
|
|
217
381
|
throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
|
|
@@ -230,22 +394,35 @@ class MeshParser {
|
|
|
230
394
|
reject(new Error(`Failed to decode Draco geometry: ${error}`));
|
|
231
395
|
});
|
|
232
396
|
});
|
|
397
|
+
// Cache the decoded geometry
|
|
398
|
+
if (cacheKey && version) {
|
|
399
|
+
const posAttr = geometry.getAttribute('position');
|
|
400
|
+
const normAttr = geometry.getAttribute('normal');
|
|
401
|
+
const uvAttr = geometry.getAttribute('uv');
|
|
402
|
+
const indexAttr = geometry.index;
|
|
403
|
+
if (posAttr && 'array' in posAttr && indexAttr && 'array' in indexAttr) {
|
|
404
|
+
AssetMemoryCache.setGeometry(cacheKey, version, {
|
|
405
|
+
positions: posAttr.array,
|
|
406
|
+
normals: (normAttr && 'array' in normAttr) ? normAttr.array : undefined,
|
|
407
|
+
uvs: (uvAttr && 'array' in uvAttr) ? uvAttr.array : undefined,
|
|
408
|
+
indices: indexAttr.array
|
|
409
|
+
}).catch(() => {
|
|
410
|
+
// Silent fail - caching is optional
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
233
414
|
return geometry;
|
|
234
415
|
}
|
|
235
416
|
/**
|
|
236
417
|
* Build Three.js scene from parsed mesh data
|
|
237
418
|
*/
|
|
238
|
-
static async buildScene(parsedData, dataBlob, dracoLoader) {
|
|
419
|
+
static async buildScene(parsedData, dataBlob, dracoLoader, cacheKeyPrefix, version) {
|
|
239
420
|
const root = new THREE__namespace.Group();
|
|
240
421
|
root.name = 'StowKitMesh';
|
|
241
422
|
const { geometries, materials, nodes, meshIndices } = parsedData;
|
|
242
423
|
// Pre-load ALL geometries in parallel for maximum speed
|
|
243
|
-
const
|
|
244
|
-
const geometryPromises = geometries.map(geoInfo => this.createGeometry(geoInfo, dataBlob, dracoLoader));
|
|
424
|
+
const geometryPromises = geometries.map((geoInfo, index) => this.createGeometry(geoInfo, dataBlob, dracoLoader, cacheKeyPrefix ? `${cacheKeyPrefix}_geo${index}` : undefined, version));
|
|
245
425
|
const loadedGeometries = await Promise.all(geometryPromises);
|
|
246
|
-
const dracoTime = performance.now() - dracoStart;
|
|
247
|
-
const totalVerts = geometries.reduce((sum, g) => sum + g.vertexCount, 0);
|
|
248
|
-
reader.PerfLogger.log(`[Perf] Draco decode: ${dracoTime.toFixed(2)}ms (${geometries.length} meshes, ${totalVerts} total vertices)`);
|
|
249
426
|
// Create all Three.js objects for nodes
|
|
250
427
|
const nodeObjects = [];
|
|
251
428
|
for (const node of nodes) {
|
|
@@ -332,11 +509,13 @@ class MeshParser {
|
|
|
332
509
|
* Represents an opened StowKit pack with methods to load assets by name
|
|
333
510
|
*/
|
|
334
511
|
class StowKitPack {
|
|
335
|
-
constructor(reader, ktx2Loader, dracoLoader) {
|
|
512
|
+
constructor(reader, ktx2Loader, dracoLoader, packUrl = '', packVersion = '') {
|
|
336
513
|
this.textureCache = new Map();
|
|
337
514
|
this.reader = reader;
|
|
338
515
|
this.ktx2Loader = ktx2Loader;
|
|
339
516
|
this.dracoLoader = dracoLoader;
|
|
517
|
+
this.packUrl = packUrl;
|
|
518
|
+
this.packVersion = packVersion || Date.now().toString();
|
|
340
519
|
}
|
|
341
520
|
/**
|
|
342
521
|
* Load a skinned mesh by its string ID
|
|
@@ -681,14 +860,10 @@ class StowKitPack {
|
|
|
681
860
|
// Parse mesh data
|
|
682
861
|
const parsedData = MeshParser.parseMeshData(metadata, data);
|
|
683
862
|
// Load textures for materials
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
reader.PerfLogger.log(`[Perf] Textures: ${textureTime.toFixed(2)}ms (${textureNames.join(', ')})`);
|
|
689
|
-
}
|
|
690
|
-
// Build Three.js scene with Draco decoder
|
|
691
|
-
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader);
|
|
863
|
+
await this.loadMaterialTextures(parsedData.materialData, parsedData.materials);
|
|
864
|
+
// Build Three.js scene with Draco decoder (with caching)
|
|
865
|
+
const cacheKey = `${this.packUrl}::${assetPath}`;
|
|
866
|
+
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader, cacheKey, this.packVersion);
|
|
692
867
|
const totalTime = performance.now() - totalStart;
|
|
693
868
|
reader.PerfLogger.log(`[Perf] === Mesh "${assetPath}": ${totalTime.toFixed(2)}ms total ===`);
|
|
694
869
|
return scene;
|
|
@@ -1036,12 +1211,24 @@ class StowKitPack {
|
|
|
1036
1211
|
return Array.from(uniqueTextures);
|
|
1037
1212
|
}
|
|
1038
1213
|
async loadKTX2Texture(data, textureName) {
|
|
1039
|
-
//
|
|
1214
|
+
// Try IndexedDB cache first
|
|
1215
|
+
const cacheKey = textureName ? `${this.packUrl}::${textureName}` : undefined;
|
|
1216
|
+
if (cacheKey) {
|
|
1217
|
+
const cached = await AssetMemoryCache.getTexture(cacheKey, this.packVersion);
|
|
1218
|
+
if (cached) {
|
|
1219
|
+
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName}": 0ms (IndexedDB cache)`);
|
|
1220
|
+
const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
|
|
1221
|
+
texture.needsUpdate = true;
|
|
1222
|
+
return texture;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// Decode using Basis transcoder
|
|
1226
|
+
const decodeStart = performance.now();
|
|
1040
1227
|
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
1041
1228
|
const blob = new Blob([arrayBuffer]);
|
|
1042
1229
|
const url = URL.createObjectURL(blob);
|
|
1043
1230
|
try {
|
|
1044
|
-
|
|
1231
|
+
const texture = await new Promise((resolve, reject) => {
|
|
1045
1232
|
this.ktx2Loader.load(url, (texture) => {
|
|
1046
1233
|
URL.revokeObjectURL(url);
|
|
1047
1234
|
texture.needsUpdate = true;
|
|
@@ -1051,6 +1238,24 @@ class StowKitPack {
|
|
|
1051
1238
|
reject(error);
|
|
1052
1239
|
});
|
|
1053
1240
|
});
|
|
1241
|
+
const decodeTime = performance.now() - decodeStart;
|
|
1242
|
+
reader.PerfLogger.log(`[Perf] ✓ Texture "${textureName || 'unknown'}": ${decodeTime.toFixed(2)}ms (Basis decode ${texture.image?.width}x${texture.image?.height})`);
|
|
1243
|
+
// Cache the transcoded texture data to IndexedDB
|
|
1244
|
+
if (cacheKey && texture.mipmaps && texture.mipmaps.length > 0) {
|
|
1245
|
+
const mipmap = texture.mipmaps[0];
|
|
1246
|
+
const dataArray = new Uint8Array(mipmap.data.buffer, mipmap.data.byteOffset, mipmap.data.byteLength);
|
|
1247
|
+
AssetMemoryCache.setTexture(cacheKey, this.packVersion, {
|
|
1248
|
+
data: dataArray,
|
|
1249
|
+
width: mipmap.width,
|
|
1250
|
+
height: mipmap.height,
|
|
1251
|
+
format: texture.format,
|
|
1252
|
+
internalFormat: texture.format,
|
|
1253
|
+
compressed: true
|
|
1254
|
+
}).catch(() => {
|
|
1255
|
+
// Silent fail - caching is optional
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return texture;
|
|
1054
1259
|
}
|
|
1055
1260
|
catch (error) {
|
|
1056
1261
|
URL.revokeObjectURL(url);
|
|
@@ -1111,17 +1316,22 @@ class StowKitLoader {
|
|
|
1111
1316
|
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
1112
1317
|
}
|
|
1113
1318
|
const arrayBuffer = await response.arrayBuffer();
|
|
1319
|
+
// Generate version for cache invalidation (based on file size)
|
|
1320
|
+
const version = `${arrayBuffer.byteLength}`;
|
|
1114
1321
|
// Create a new reader instance for this pack
|
|
1115
1322
|
const reader$1 = new reader.StowKitReader(this.wasmPath);
|
|
1116
1323
|
await reader$1.init();
|
|
1117
1324
|
await reader$1.open(arrayBuffer);
|
|
1118
1325
|
// Return pack wrapper with its own dedicated reader
|
|
1119
|
-
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader);
|
|
1326
|
+
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader, url, version);
|
|
1120
1327
|
}
|
|
1121
1328
|
/**
|
|
1122
1329
|
* Load a .stow pack from memory (ArrayBuffer, Blob, or File)
|
|
1330
|
+
* @param data - The .stow file data
|
|
1331
|
+
* @param options - Loader options
|
|
1332
|
+
* @param cacheKey - Optional unique identifier for caching (e.g., CDN URL). Recommended for files loaded from CDN.
|
|
1123
1333
|
*/
|
|
1124
|
-
static async loadFromMemory(data, options) {
|
|
1334
|
+
static async loadFromMemory(data, options, cacheKey) {
|
|
1125
1335
|
// Initialize loaders if needed
|
|
1126
1336
|
if (!this.initialized) {
|
|
1127
1337
|
await this.initialize(options);
|
|
@@ -1140,12 +1350,15 @@ class StowKitLoader {
|
|
|
1140
1350
|
else {
|
|
1141
1351
|
throw new Error('Data must be ArrayBuffer, Blob, or File');
|
|
1142
1352
|
}
|
|
1353
|
+
// Generate version for cache invalidation (based on file size)
|
|
1354
|
+
const version = `${arrayBuffer.byteLength}`;
|
|
1355
|
+
const packUrl = cacheKey || 'memory';
|
|
1143
1356
|
// Create a new reader instance for this pack
|
|
1144
1357
|
const reader$1 = new reader.StowKitReader(this.wasmPath);
|
|
1145
1358
|
await reader$1.init();
|
|
1146
1359
|
await reader$1.open(arrayBuffer);
|
|
1147
1360
|
// Return pack wrapper with its own dedicated reader
|
|
1148
|
-
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader);
|
|
1361
|
+
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader, packUrl, version);
|
|
1149
1362
|
}
|
|
1150
1363
|
/**
|
|
1151
1364
|
* Initialize the loader (called automatically on first load)
|
|
@@ -1180,6 +1393,7 @@ Object.defineProperty(exports, 'PerfLogger', {
|
|
|
1180
1393
|
enumerable: true,
|
|
1181
1394
|
get: function () { return reader.PerfLogger; }
|
|
1182
1395
|
});
|
|
1396
|
+
exports.AssetMemoryCache = AssetMemoryCache;
|
|
1183
1397
|
exports.StowKitLoader = StowKitLoader;
|
|
1184
1398
|
exports.StowKitPack = StowKitPack;
|
|
1185
1399
|
//# sourceMappingURL=stowkit-three-loader.js.map
|