@stowkit/three-loader 0.1.30 → 0.1.31
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 +236 -12
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +236 -11
- 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,24 @@ 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
|
+
reader.PerfLogger.log(`[Perf] Geometry cache hit: ${cacheKey}`);
|
|
368
|
+
const geometry = new THREE__namespace.BufferGeometry();
|
|
369
|
+
geometry.setAttribute('position', new THREE__namespace.BufferAttribute(cached.positions, 3));
|
|
370
|
+
if (cached.normals) {
|
|
371
|
+
geometry.setAttribute('normal', new THREE__namespace.BufferAttribute(cached.normals, 3));
|
|
372
|
+
}
|
|
373
|
+
if (cached.uvs) {
|
|
374
|
+
geometry.setAttribute('uv', new THREE__namespace.BufferAttribute(cached.uvs, 2));
|
|
375
|
+
}
|
|
376
|
+
geometry.setIndex(new THREE__namespace.BufferAttribute(cached.indices, 1));
|
|
377
|
+
return geometry;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
215
380
|
// Extract the Draco compressed buffer
|
|
216
381
|
if (geoInfo.compressedBufferOffset + geoInfo.compressedBufferSize > dataBlob.length) {
|
|
217
382
|
throw new Error(`Compressed buffer out of bounds: offset=${geoInfo.compressedBufferOffset}, size=${geoInfo.compressedBufferSize}, dataLength=${dataBlob.length}`);
|
|
@@ -230,22 +395,39 @@ class MeshParser {
|
|
|
230
395
|
reject(new Error(`Failed to decode Draco geometry: ${error}`));
|
|
231
396
|
});
|
|
232
397
|
});
|
|
398
|
+
// Cache the decoded geometry
|
|
399
|
+
if (cacheKey && version) {
|
|
400
|
+
const posAttr = geometry.getAttribute('position');
|
|
401
|
+
const normAttr = geometry.getAttribute('normal');
|
|
402
|
+
const uvAttr = geometry.getAttribute('uv');
|
|
403
|
+
const indexAttr = geometry.index;
|
|
404
|
+
if (posAttr && 'array' in posAttr && indexAttr && 'array' in indexAttr) {
|
|
405
|
+
AssetMemoryCache.setGeometry(cacheKey, version, {
|
|
406
|
+
positions: posAttr.array,
|
|
407
|
+
normals: (normAttr && 'array' in normAttr) ? normAttr.array : undefined,
|
|
408
|
+
uvs: (uvAttr && 'array' in uvAttr) ? uvAttr.array : undefined,
|
|
409
|
+
indices: indexAttr.array
|
|
410
|
+
}).catch(() => {
|
|
411
|
+
// Silent fail - caching is optional
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
233
415
|
return geometry;
|
|
234
416
|
}
|
|
235
417
|
/**
|
|
236
418
|
* Build Three.js scene from parsed mesh data
|
|
237
419
|
*/
|
|
238
|
-
static async buildScene(parsedData, dataBlob, dracoLoader) {
|
|
420
|
+
static async buildScene(parsedData, dataBlob, dracoLoader, cacheKeyPrefix, version) {
|
|
239
421
|
const root = new THREE__namespace.Group();
|
|
240
422
|
root.name = 'StowKitMesh';
|
|
241
423
|
const { geometries, materials, nodes, meshIndices } = parsedData;
|
|
242
424
|
// Pre-load ALL geometries in parallel for maximum speed
|
|
243
425
|
const dracoStart = performance.now();
|
|
244
|
-
const geometryPromises = geometries.map(geoInfo => this.createGeometry(geoInfo, dataBlob, dracoLoader));
|
|
426
|
+
const geometryPromises = geometries.map((geoInfo, index) => this.createGeometry(geoInfo, dataBlob, dracoLoader, cacheKeyPrefix ? `${cacheKeyPrefix}_geo${index}` : undefined, version));
|
|
245
427
|
const loadedGeometries = await Promise.all(geometryPromises);
|
|
246
428
|
const dracoTime = performance.now() - dracoStart;
|
|
247
429
|
const totalVerts = geometries.reduce((sum, g) => sum + g.vertexCount, 0);
|
|
248
|
-
reader.PerfLogger.log(`[Perf] Draco
|
|
430
|
+
reader.PerfLogger.log(`[Perf] Draco: ${dracoTime.toFixed(2)}ms (${geometries.length} meshes, ${totalVerts} verts)`);
|
|
249
431
|
// Create all Three.js objects for nodes
|
|
250
432
|
const nodeObjects = [];
|
|
251
433
|
for (const node of nodes) {
|
|
@@ -332,11 +514,13 @@ class MeshParser {
|
|
|
332
514
|
* Represents an opened StowKit pack with methods to load assets by name
|
|
333
515
|
*/
|
|
334
516
|
class StowKitPack {
|
|
335
|
-
constructor(reader, ktx2Loader, dracoLoader) {
|
|
517
|
+
constructor(reader, ktx2Loader, dracoLoader, packUrl = '', packVersion = '') {
|
|
336
518
|
this.textureCache = new Map();
|
|
337
519
|
this.reader = reader;
|
|
338
520
|
this.ktx2Loader = ktx2Loader;
|
|
339
521
|
this.dracoLoader = dracoLoader;
|
|
522
|
+
this.packUrl = packUrl;
|
|
523
|
+
this.packVersion = packVersion || Date.now().toString();
|
|
340
524
|
}
|
|
341
525
|
/**
|
|
342
526
|
* Load a skinned mesh by its string ID
|
|
@@ -687,8 +871,9 @@ class StowKitPack {
|
|
|
687
871
|
if (textureNames.length > 0) {
|
|
688
872
|
reader.PerfLogger.log(`[Perf] Textures: ${textureTime.toFixed(2)}ms (${textureNames.join(', ')})`);
|
|
689
873
|
}
|
|
690
|
-
// Build Three.js scene with Draco decoder
|
|
691
|
-
const
|
|
874
|
+
// Build Three.js scene with Draco decoder (with caching)
|
|
875
|
+
const cacheKey = `${this.packUrl}::${assetPath}`;
|
|
876
|
+
const scene = await MeshParser.buildScene(parsedData, data, this.dracoLoader, cacheKey, this.packVersion);
|
|
692
877
|
const totalTime = performance.now() - totalStart;
|
|
693
878
|
reader.PerfLogger.log(`[Perf] === Mesh "${assetPath}": ${totalTime.toFixed(2)}ms total ===`);
|
|
694
879
|
return scene;
|
|
@@ -1036,14 +1221,29 @@ class StowKitPack {
|
|
|
1036
1221
|
return Array.from(uniqueTextures);
|
|
1037
1222
|
}
|
|
1038
1223
|
async loadKTX2Texture(data, textureName) {
|
|
1224
|
+
// Try IndexedDB cache first
|
|
1225
|
+
const cacheKey = textureName ? `${this.packUrl}::${textureName}` : undefined;
|
|
1226
|
+
if (cacheKey) {
|
|
1227
|
+
const startCache = performance.now();
|
|
1228
|
+
const cached = await AssetMemoryCache.getTexture(cacheKey, this.packVersion);
|
|
1229
|
+
if (cached) {
|
|
1230
|
+
reader.PerfLogger.log(`[Perf] Texture cache hit (IndexedDB): ${textureName} (${(performance.now() - startCache).toFixed(2)}ms)`);
|
|
1231
|
+
const texture = new THREE__namespace.CompressedTexture([{ data: cached.data, width: cached.width, height: cached.height }], cached.width, cached.height, cached.format, THREE__namespace.UnsignedByteType);
|
|
1232
|
+
texture.needsUpdate = true;
|
|
1233
|
+
return texture;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1039
1236
|
// Create blob URL - KTX2Loader requires a URL, can't use ArrayBuffer directly
|
|
1040
1237
|
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
1041
1238
|
const blob = new Blob([arrayBuffer]);
|
|
1042
1239
|
const url = URL.createObjectURL(blob);
|
|
1043
1240
|
try {
|
|
1044
|
-
|
|
1241
|
+
const decodeStart = performance.now();
|
|
1242
|
+
const texture = await new Promise((resolve, reject) => {
|
|
1045
1243
|
this.ktx2Loader.load(url, (texture) => {
|
|
1046
1244
|
URL.revokeObjectURL(url);
|
|
1245
|
+
const decodeTime = performance.now() - decodeStart;
|
|
1246
|
+
reader.PerfLogger.log(`[Perf] Basis decode: ${decodeTime.toFixed(2)}ms (${textureName || 'unknown'}, ${texture.image?.width}x${texture.image?.height})`);
|
|
1047
1247
|
texture.needsUpdate = true;
|
|
1048
1248
|
resolve(texture);
|
|
1049
1249
|
}, undefined, (error) => {
|
|
@@ -1051,6 +1251,22 @@ class StowKitPack {
|
|
|
1051
1251
|
reject(error);
|
|
1052
1252
|
});
|
|
1053
1253
|
});
|
|
1254
|
+
// Cache the transcoded texture data to IndexedDB
|
|
1255
|
+
if (cacheKey && texture.mipmaps && texture.mipmaps.length > 0) {
|
|
1256
|
+
const mipmap = texture.mipmaps[0];
|
|
1257
|
+
const dataArray = new Uint8Array(mipmap.data.buffer, mipmap.data.byteOffset, mipmap.data.byteLength);
|
|
1258
|
+
AssetMemoryCache.setTexture(cacheKey, this.packVersion, {
|
|
1259
|
+
data: dataArray,
|
|
1260
|
+
width: mipmap.width,
|
|
1261
|
+
height: mipmap.height,
|
|
1262
|
+
format: texture.format,
|
|
1263
|
+
internalFormat: texture.format,
|
|
1264
|
+
compressed: true
|
|
1265
|
+
}).catch(() => {
|
|
1266
|
+
// Silent fail - caching is optional
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
return texture;
|
|
1054
1270
|
}
|
|
1055
1271
|
catch (error) {
|
|
1056
1272
|
URL.revokeObjectURL(url);
|
|
@@ -1111,17 +1327,22 @@ class StowKitLoader {
|
|
|
1111
1327
|
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
1112
1328
|
}
|
|
1113
1329
|
const arrayBuffer = await response.arrayBuffer();
|
|
1330
|
+
// Generate version for cache invalidation (based on file size)
|
|
1331
|
+
const version = `${arrayBuffer.byteLength}`;
|
|
1114
1332
|
// Create a new reader instance for this pack
|
|
1115
1333
|
const reader$1 = new reader.StowKitReader(this.wasmPath);
|
|
1116
1334
|
await reader$1.init();
|
|
1117
1335
|
await reader$1.open(arrayBuffer);
|
|
1118
1336
|
// Return pack wrapper with its own dedicated reader
|
|
1119
|
-
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader);
|
|
1337
|
+
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader, url, version);
|
|
1120
1338
|
}
|
|
1121
1339
|
/**
|
|
1122
1340
|
* Load a .stow pack from memory (ArrayBuffer, Blob, or File)
|
|
1341
|
+
* @param data - The .stow file data
|
|
1342
|
+
* @param options - Loader options
|
|
1343
|
+
* @param cacheKey - Optional unique identifier for caching (e.g., CDN URL). Recommended for files loaded from CDN.
|
|
1123
1344
|
*/
|
|
1124
|
-
static async loadFromMemory(data, options) {
|
|
1345
|
+
static async loadFromMemory(data, options, cacheKey) {
|
|
1125
1346
|
// Initialize loaders if needed
|
|
1126
1347
|
if (!this.initialized) {
|
|
1127
1348
|
await this.initialize(options);
|
|
@@ -1140,12 +1361,15 @@ class StowKitLoader {
|
|
|
1140
1361
|
else {
|
|
1141
1362
|
throw new Error('Data must be ArrayBuffer, Blob, or File');
|
|
1142
1363
|
}
|
|
1364
|
+
// Generate version for cache invalidation (based on file size)
|
|
1365
|
+
const version = `${arrayBuffer.byteLength}`;
|
|
1366
|
+
const packUrl = cacheKey || 'memory';
|
|
1143
1367
|
// Create a new reader instance for this pack
|
|
1144
1368
|
const reader$1 = new reader.StowKitReader(this.wasmPath);
|
|
1145
1369
|
await reader$1.init();
|
|
1146
1370
|
await reader$1.open(arrayBuffer);
|
|
1147
1371
|
// Return pack wrapper with its own dedicated reader
|
|
1148
|
-
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader);
|
|
1372
|
+
return new StowKitPack(reader$1, this.ktx2Loader, this.dracoLoader, packUrl, version);
|
|
1149
1373
|
}
|
|
1150
1374
|
/**
|
|
1151
1375
|
* Initialize the loader (called automatically on first load)
|
|
@@ -1180,6 +1404,7 @@ Object.defineProperty(exports, 'PerfLogger', {
|
|
|
1180
1404
|
enumerable: true,
|
|
1181
1405
|
get: function () { return reader.PerfLogger; }
|
|
1182
1406
|
});
|
|
1407
|
+
exports.AssetMemoryCache = AssetMemoryCache;
|
|
1183
1408
|
exports.StowKitLoader = StowKitLoader;
|
|
1184
1409
|
exports.StowKitPack = StowKitPack;
|
|
1185
1410
|
//# sourceMappingURL=stowkit-three-loader.js.map
|