@ifc-lite/viewer 1.0.0 → 1.1.0
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/assets/{geometry.worker-DpnHtNr3.ts → geometry.worker-CiROVpKV.ts} +6 -9
- package/dist/assets/ifc-lite_bg-BQVS1Fh7.wasm +0 -0
- package/dist/assets/index-CvYpbxNF.js +728 -0
- package/dist/ifc-lite_bg.wasm +0 -0
- package/dist/index.html +1 -1
- package/package.json +9 -9
- package/public/ifc-lite_bg.wasm +0 -0
- package/src/components/viewer/Viewport.tsx +198 -66
- package/src/components/viewer/ViewportContainer.tsx +1 -1
- package/src/hooks/useIfc.ts +235 -20
- package/src/services/ifc-cache.ts +251 -0
- package/src/store.ts +55 -4
- package/tsconfig.json +5 -1
- package/vite.config.ts +15 -1
- package/dist/assets/ifc_lite_wasm_bg-Cd3m3f2h.wasm +0 -0
- package/dist/assets/index-Dzz3WVwq.js +0 -637
- package/dist/ifc_lite_wasm_bg.wasm +0 -0
- package/public/ifc_lite_wasm_bg.wasm +0 -0
package/src/hooks/useIfc.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Hook for loading and processing IFC files
|
|
7
|
+
* Includes binary cache support for fast subsequent loads
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { useMemo, useCallback, useRef } from 'react';
|
|
@@ -13,6 +14,17 @@ import { GeometryProcessor, GeometryQuality, type MeshData } from '@ifc-lite/geo
|
|
|
13
14
|
import { IfcQuery } from '@ifc-lite/query';
|
|
14
15
|
import { BufferBuilder } from '@ifc-lite/geometry';
|
|
15
16
|
import { buildSpatialIndex } from '@ifc-lite/spatial';
|
|
17
|
+
import {
|
|
18
|
+
BinaryCacheWriter,
|
|
19
|
+
BinaryCacheReader,
|
|
20
|
+
xxhash64Hex,
|
|
21
|
+
type IfcDataStore as CacheDataStore,
|
|
22
|
+
type GeometryData,
|
|
23
|
+
} from '@ifc-lite/cache';
|
|
24
|
+
import { getCached, setCached } from '../services/ifc-cache.js';
|
|
25
|
+
|
|
26
|
+
// Minimum file size to cache (10MB) - smaller files parse quickly anyway
|
|
27
|
+
const CACHE_SIZE_THRESHOLD = 10 * 1024 * 1024;
|
|
16
28
|
|
|
17
29
|
export function useIfc() {
|
|
18
30
|
const {
|
|
@@ -33,19 +45,160 @@ export function useIfc() {
|
|
|
33
45
|
// Track if we've already logged for this ifcDataStore
|
|
34
46
|
const lastLoggedDataStoreRef = useRef<typeof ifcDataStore>(null);
|
|
35
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Load from binary cache (INSTANT path)
|
|
50
|
+
* Key optimizations:
|
|
51
|
+
* 1. Single setGeometryResult call instead of batched appendGeometryBatch
|
|
52
|
+
* 2. Build spatial index in requestIdleCallback (non-blocking)
|
|
53
|
+
*/
|
|
54
|
+
const loadFromCache = useCallback(async (
|
|
55
|
+
cacheBuffer: ArrayBuffer,
|
|
56
|
+
fileName: string
|
|
57
|
+
): Promise<boolean> => {
|
|
58
|
+
try {
|
|
59
|
+
console.time('[useIfc] cache-load');
|
|
60
|
+
setProgress({ phase: 'Loading from cache', percent: 10 });
|
|
61
|
+
|
|
62
|
+
// IMPORTANT: Reset geometry first so Viewport detects this as a new file
|
|
63
|
+
// This ensures camera fitting and bounds are properly reset
|
|
64
|
+
setGeometryResult(null);
|
|
65
|
+
|
|
66
|
+
const reader = new BinaryCacheReader();
|
|
67
|
+
const result = await reader.read(cacheBuffer);
|
|
68
|
+
|
|
69
|
+
// Convert cache data store to viewer data store format
|
|
70
|
+
const dataStore = result.dataStore as any;
|
|
71
|
+
|
|
72
|
+
if (result.geometry) {
|
|
73
|
+
const { meshes, coordinateInfo, totalVertices, totalTriangles } = result.geometry;
|
|
74
|
+
|
|
75
|
+
// INSTANT: Set ALL geometry in ONE state update (no batching!)
|
|
76
|
+
setGeometryResult({
|
|
77
|
+
meshes,
|
|
78
|
+
totalVertices,
|
|
79
|
+
totalTriangles,
|
|
80
|
+
coordinateInfo,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Set data store
|
|
84
|
+
setIfcDataStore(dataStore);
|
|
85
|
+
|
|
86
|
+
// Build spatial index in background (non-blocking)
|
|
87
|
+
if (meshes.length > 0) {
|
|
88
|
+
// Use requestIdleCallback for non-blocking BVH build
|
|
89
|
+
const buildIndex = () => {
|
|
90
|
+
console.time('[useIfc] spatial-index-background');
|
|
91
|
+
try {
|
|
92
|
+
const spatialIndex = buildSpatialIndex(meshes);
|
|
93
|
+
dataStore.spatialIndex = spatialIndex;
|
|
94
|
+
// Update store with spatial index (doesn't affect rendering)
|
|
95
|
+
setIfcDataStore({ ...dataStore });
|
|
96
|
+
console.timeEnd('[useIfc] spatial-index-background');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Schedule for idle time, or fallback to setTimeout
|
|
103
|
+
if ('requestIdleCallback' in window) {
|
|
104
|
+
(window as any).requestIdleCallback(buildIndex, { timeout: 1000 });
|
|
105
|
+
} else {
|
|
106
|
+
setTimeout(buildIndex, 100);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
setIfcDataStore(dataStore);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setProgress({ phase: 'Complete (from cache)', percent: 100 });
|
|
114
|
+
console.timeEnd('[useIfc] cache-load');
|
|
115
|
+
console.log(`[useIfc] INSTANT load: ${fileName} from cache (${result.geometry?.meshes.length || 0} meshes)`);
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('[useIfc] Failed to load from cache:', err);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}, [setProgress, setIfcDataStore, setGeometryResult]);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Save to binary cache (background operation)
|
|
126
|
+
*/
|
|
127
|
+
const saveToCache = useCallback(async (
|
|
128
|
+
cacheKey: string,
|
|
129
|
+
dataStore: any,
|
|
130
|
+
geometry: GeometryData,
|
|
131
|
+
sourceBuffer: ArrayBuffer,
|
|
132
|
+
fileName: string
|
|
133
|
+
): Promise<void> => {
|
|
134
|
+
try {
|
|
135
|
+
console.time('[useIfc] cache-write');
|
|
136
|
+
|
|
137
|
+
const writer = new BinaryCacheWriter();
|
|
138
|
+
|
|
139
|
+
// Adapt dataStore to cache format
|
|
140
|
+
const cacheDataStore: CacheDataStore = {
|
|
141
|
+
schema: dataStore.schemaVersion === 'IFC4' ? 1 : dataStore.schemaVersion === 'IFC4X3' ? 2 : 0,
|
|
142
|
+
entityCount: dataStore.entityCount || dataStore.entities?.count || 0,
|
|
143
|
+
strings: dataStore.strings,
|
|
144
|
+
entities: dataStore.entities,
|
|
145
|
+
properties: dataStore.properties,
|
|
146
|
+
quantities: dataStore.quantities,
|
|
147
|
+
relationships: dataStore.relationships,
|
|
148
|
+
spatialHierarchy: dataStore.spatialHierarchy,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const cacheBuffer = await writer.write(
|
|
152
|
+
cacheDataStore,
|
|
153
|
+
geometry,
|
|
154
|
+
sourceBuffer,
|
|
155
|
+
{ includeGeometry: true }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await setCached(cacheKey, cacheBuffer, fileName, sourceBuffer.byteLength);
|
|
159
|
+
|
|
160
|
+
console.timeEnd('[useIfc] cache-write');
|
|
161
|
+
console.log(`[useIfc] Cached ${fileName} (${(cacheBuffer.byteLength / 1024 / 1024).toFixed(2)}MB cache)`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.warn('[useIfc] Failed to cache model:', err);
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
36
167
|
const loadFile = useCallback(async (file: File) => {
|
|
37
168
|
const { resetViewerState } = useViewerStore.getState();
|
|
38
|
-
|
|
169
|
+
|
|
39
170
|
try {
|
|
40
171
|
// Reset all viewer state before loading new file
|
|
41
172
|
resetViewerState();
|
|
42
|
-
|
|
173
|
+
|
|
43
174
|
setLoading(true);
|
|
44
175
|
setError(null);
|
|
45
176
|
setProgress({ phase: 'Loading file', percent: 0 });
|
|
46
177
|
|
|
47
178
|
// Read file
|
|
48
179
|
const buffer = await file.arrayBuffer();
|
|
180
|
+
const fileSizeMB = buffer.byteLength / (1024 * 1024);
|
|
181
|
+
|
|
182
|
+
// Compute cache key (hash of file content)
|
|
183
|
+
setProgress({ phase: 'Checking cache', percent: 5 });
|
|
184
|
+
const cacheKey = xxhash64Hex(buffer);
|
|
185
|
+
console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, hash: ${cacheKey}`);
|
|
186
|
+
|
|
187
|
+
// Try to load from cache first (only for files above threshold)
|
|
188
|
+
if (buffer.byteLength >= CACHE_SIZE_THRESHOLD) {
|
|
189
|
+
const cachedBuffer = await getCached(cacheKey);
|
|
190
|
+
if (cachedBuffer) {
|
|
191
|
+
const success = await loadFromCache(cachedBuffer, file.name);
|
|
192
|
+
if (success) {
|
|
193
|
+
setLoading(false);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Cache load failed, fall through to normal parsing
|
|
197
|
+
console.log('[useIfc] Cache load failed, falling back to parsing');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Cache miss or small file - parse normally
|
|
49
202
|
setProgress({ phase: 'Parsing IFC', percent: 10 });
|
|
50
203
|
|
|
51
204
|
// Parse IFC using columnar parser
|
|
@@ -78,64 +231,126 @@ export function useIfc() {
|
|
|
78
231
|
}
|
|
79
232
|
}
|
|
80
233
|
|
|
81
|
-
// Use
|
|
82
|
-
const bufferBuilder = new BufferBuilder();
|
|
234
|
+
// Use adaptive processing: sync for small files, streaming for large files
|
|
83
235
|
let estimatedTotal = 0;
|
|
84
236
|
let totalMeshes = 0;
|
|
85
237
|
const allMeshes: MeshData[] = []; // Collect all meshes for BVH building
|
|
238
|
+
let finalCoordinateInfo: any = null;
|
|
86
239
|
|
|
87
240
|
// Clear existing geometry result
|
|
88
241
|
setGeometryResult(null);
|
|
89
242
|
|
|
243
|
+
// Timing instrumentation
|
|
244
|
+
const processingStart = performance.now();
|
|
245
|
+
let batchCount = 0;
|
|
246
|
+
let lastBatchTime = processingStart;
|
|
247
|
+
let totalWaitTime = 0; // Time waiting for WASM to yield batches
|
|
248
|
+
let totalProcessTime = 0; // Time processing batches in JS
|
|
249
|
+
|
|
90
250
|
try {
|
|
91
|
-
|
|
251
|
+
console.log(`[useIfc] Starting adaptive processing (file size: ${fileSizeMB.toFixed(2)}MB)...`);
|
|
252
|
+
console.time('[useIfc] total-processing');
|
|
253
|
+
|
|
254
|
+
for await (const event of geometryProcessor.processAdaptive(new Uint8Array(buffer), {
|
|
255
|
+
sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
|
|
256
|
+
batchSize: 25,
|
|
257
|
+
entityIndex: entityIndexMap,
|
|
258
|
+
})) {
|
|
259
|
+
const eventReceived = performance.now();
|
|
260
|
+
const waitTime = eventReceived - lastBatchTime;
|
|
261
|
+
|
|
92
262
|
switch (event.type) {
|
|
93
263
|
case 'start':
|
|
94
264
|
estimatedTotal = event.totalEstimate;
|
|
265
|
+
console.log(`[useIfc] Processing started, estimated: ${estimatedTotal}`);
|
|
95
266
|
break;
|
|
96
267
|
case 'model-open':
|
|
97
268
|
setProgress({ phase: 'Processing geometry', percent: 50 });
|
|
269
|
+
console.log(`[useIfc] Model opened at ${(eventReceived - processingStart).toFixed(0)}ms`);
|
|
98
270
|
break;
|
|
99
|
-
case 'batch':
|
|
271
|
+
case 'batch': {
|
|
272
|
+
batchCount++;
|
|
273
|
+
totalWaitTime += waitTime;
|
|
274
|
+
|
|
275
|
+
const processStart = performance.now();
|
|
276
|
+
|
|
100
277
|
// Collect meshes for BVH building
|
|
101
278
|
allMeshes.push(...event.meshes);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
appendGeometryBatch(
|
|
279
|
+
finalCoordinateInfo = event.coordinateInfo;
|
|
280
|
+
|
|
281
|
+
// Append mesh batch to store (triggers React re-render)
|
|
282
|
+
appendGeometryBatch(event.meshes, event.coordinateInfo);
|
|
106
283
|
totalMeshes = event.totalSoFar;
|
|
107
284
|
|
|
108
285
|
// Update progress (50-95% for geometry processing)
|
|
109
|
-
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal, totalMeshes)) * 45);
|
|
286
|
+
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes)) * 45);
|
|
110
287
|
setProgress({
|
|
111
288
|
phase: `Rendering geometry (${totalMeshes} meshes)`,
|
|
112
289
|
percent: progressPercent
|
|
113
290
|
});
|
|
291
|
+
|
|
292
|
+
const processTime = performance.now() - processStart;
|
|
293
|
+
totalProcessTime += processTime;
|
|
294
|
+
|
|
295
|
+
// Log batch timing (first 5, then every 10th)
|
|
296
|
+
if (batchCount <= 5 || batchCount % 10 === 0) {
|
|
297
|
+
console.log(
|
|
298
|
+
`[useIfc] Batch #${batchCount}: ${event.meshes.length} meshes, ` +
|
|
299
|
+
`wait: ${waitTime.toFixed(0)}ms, process: ${processTime.toFixed(0)}ms, ` +
|
|
300
|
+
`total: ${totalMeshes} meshes at ${(eventReceived - processingStart).toFixed(0)}ms`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
114
303
|
break;
|
|
304
|
+
}
|
|
115
305
|
case 'complete':
|
|
306
|
+
console.log(
|
|
307
|
+
`[useIfc] Processing complete: ${batchCount} batches, ${event.totalMeshes} meshes\n` +
|
|
308
|
+
` Total wait (WASM): ${totalWaitTime.toFixed(0)}ms\n` +
|
|
309
|
+
` Total process (JS): ${totalProcessTime.toFixed(0)}ms\n` +
|
|
310
|
+
` First batch at: ${batchCount > 0 ? '(see Batch #1 above)' : 'N/A'}`
|
|
311
|
+
);
|
|
312
|
+
console.timeEnd('[useIfc] total-processing');
|
|
313
|
+
|
|
314
|
+
finalCoordinateInfo = event.coordinateInfo;
|
|
315
|
+
|
|
116
316
|
// Update geometry result with final coordinate info
|
|
117
317
|
updateCoordinateInfo(event.coordinateInfo);
|
|
118
|
-
|
|
119
|
-
// Build spatial index from
|
|
318
|
+
|
|
319
|
+
// Build spatial index from meshes
|
|
120
320
|
if (allMeshes.length > 0) {
|
|
121
321
|
setProgress({ phase: 'Building spatial index', percent: 95 });
|
|
322
|
+
console.time('[useIfc] spatial-index');
|
|
122
323
|
try {
|
|
123
324
|
const spatialIndex = buildSpatialIndex(allMeshes);
|
|
124
|
-
// Attach spatial index to dataStore
|
|
125
325
|
(dataStore as any).spatialIndex = spatialIndex;
|
|
126
|
-
setIfcDataStore(dataStore);
|
|
326
|
+
setIfcDataStore(dataStore);
|
|
327
|
+
console.timeEnd('[useIfc] spatial-index');
|
|
127
328
|
} catch (err) {
|
|
329
|
+
console.timeEnd('[useIfc] spatial-index');
|
|
128
330
|
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
129
|
-
// Continue without spatial index - it's optional
|
|
130
331
|
}
|
|
131
332
|
}
|
|
132
|
-
|
|
333
|
+
|
|
133
334
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
335
|
+
|
|
336
|
+
// Cache the result in the background (for files above threshold)
|
|
337
|
+
if (buffer.byteLength >= CACHE_SIZE_THRESHOLD && allMeshes.length > 0 && finalCoordinateInfo) {
|
|
338
|
+
// Don't await - let it run in background
|
|
339
|
+
const geometryData: GeometryData = {
|
|
340
|
+
meshes: allMeshes,
|
|
341
|
+
totalVertices: allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0),
|
|
342
|
+
totalTriangles: allMeshes.reduce((sum, m) => sum + m.indices.length / 3, 0),
|
|
343
|
+
coordinateInfo: finalCoordinateInfo,
|
|
344
|
+
};
|
|
345
|
+
saveToCache(cacheKey, dataStore, geometryData, buffer, file.name);
|
|
346
|
+
}
|
|
134
347
|
break;
|
|
135
348
|
}
|
|
349
|
+
|
|
350
|
+
lastBatchTime = performance.now();
|
|
136
351
|
}
|
|
137
352
|
} catch (err) {
|
|
138
|
-
console.error('[useIfc] Error in
|
|
353
|
+
console.error('[useIfc] Error in processing:', err);
|
|
139
354
|
setError(err instanceof Error ? err.message : 'Unknown error during geometry processing');
|
|
140
355
|
}
|
|
141
356
|
|
|
@@ -144,12 +359,12 @@ export function useIfc() {
|
|
|
144
359
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
145
360
|
setLoading(false);
|
|
146
361
|
}
|
|
147
|
-
}, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, appendGeometryBatch, updateCoordinateInfo]);
|
|
362
|
+
}, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, appendGeometryBatch, updateCoordinateInfo, loadFromCache, saveToCache]);
|
|
148
363
|
|
|
149
364
|
// Memoize query to prevent recreation on every render
|
|
150
365
|
const query = useMemo(() => {
|
|
151
366
|
if (!ifcDataStore) return null;
|
|
152
|
-
|
|
367
|
+
|
|
153
368
|
// Only log once per ifcDataStore
|
|
154
369
|
lastLoggedDataStoreRef.current = ifcDataStore;
|
|
155
370
|
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IndexedDB cache service for IFC files
|
|
7
|
+
*
|
|
8
|
+
* Stores parsed IFC data and geometry in IndexedDB for fast subsequent loads.
|
|
9
|
+
* Uses xxhash64 of the source file as the cache key.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DB_NAME = 'ifc-lite-cache';
|
|
13
|
+
const DB_VERSION = 1;
|
|
14
|
+
const STORE_NAME = 'models';
|
|
15
|
+
|
|
16
|
+
interface CacheEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
buffer: ArrayBuffer;
|
|
19
|
+
fileName: string;
|
|
20
|
+
fileSize: number;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Open the IndexedDB database
|
|
28
|
+
*/
|
|
29
|
+
function openDatabase(): Promise<IDBDatabase> {
|
|
30
|
+
if (dbPromise) return dbPromise;
|
|
31
|
+
|
|
32
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
33
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
34
|
+
|
|
35
|
+
request.onerror = () => {
|
|
36
|
+
console.error('[IFC Cache] Failed to open database:', request.error);
|
|
37
|
+
dbPromise = null; // Reset so we can retry
|
|
38
|
+
reject(request.error);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
request.onsuccess = () => {
|
|
42
|
+
const db = request.result;
|
|
43
|
+
|
|
44
|
+
// Verify the object store exists (handles corrupted DB state)
|
|
45
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
46
|
+
console.warn('[IFC Cache] Object store missing, recreating database...');
|
|
47
|
+
db.close();
|
|
48
|
+
dbPromise = null;
|
|
49
|
+
|
|
50
|
+
// Delete and recreate the database
|
|
51
|
+
const deleteRequest = indexedDB.deleteDatabase(DB_NAME);
|
|
52
|
+
deleteRequest.onsuccess = () => {
|
|
53
|
+
// Retry opening after deletion
|
|
54
|
+
openDatabase().then(resolve).catch(reject);
|
|
55
|
+
};
|
|
56
|
+
deleteRequest.onerror = () => {
|
|
57
|
+
reject(new Error('Failed to recreate database'));
|
|
58
|
+
};
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resolve(db);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
request.onupgradeneeded = (event) => {
|
|
66
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
67
|
+
|
|
68
|
+
// Create object store for cached models
|
|
69
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
70
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' });
|
|
71
|
+
store.createIndex('createdAt', 'createdAt', { unique: false });
|
|
72
|
+
store.createIndex('fileName', 'fileName', { unique: false });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return dbPromise;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a cached model by hash key
|
|
82
|
+
*/
|
|
83
|
+
export async function getCached(key: string): Promise<ArrayBuffer | null> {
|
|
84
|
+
try {
|
|
85
|
+
const db = await openDatabase();
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
88
|
+
const store = tx.objectStore(STORE_NAME);
|
|
89
|
+
const request = store.get(key);
|
|
90
|
+
|
|
91
|
+
request.onsuccess = () => {
|
|
92
|
+
const entry = request.result as CacheEntry | undefined;
|
|
93
|
+
if (entry) {
|
|
94
|
+
console.log(`[IFC Cache] Cache hit for ${entry.fileName} (${(entry.fileSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
95
|
+
resolve(entry.buffer);
|
|
96
|
+
} else {
|
|
97
|
+
resolve(null);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
request.onerror = () => {
|
|
102
|
+
console.error('[IFC Cache] Failed to get cache entry:', request.error);
|
|
103
|
+
reject(request.error);
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('[IFC Cache] Cache read failed:', err);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Store a model in the cache
|
|
114
|
+
*/
|
|
115
|
+
export async function setCached(
|
|
116
|
+
key: string,
|
|
117
|
+
buffer: ArrayBuffer,
|
|
118
|
+
fileName: string,
|
|
119
|
+
fileSize: number
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
const db = await openDatabase();
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
125
|
+
const store = tx.objectStore(STORE_NAME);
|
|
126
|
+
|
|
127
|
+
const entry: CacheEntry = {
|
|
128
|
+
key,
|
|
129
|
+
buffer,
|
|
130
|
+
fileName,
|
|
131
|
+
fileSize,
|
|
132
|
+
createdAt: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const request = store.put(entry);
|
|
136
|
+
|
|
137
|
+
request.onsuccess = () => {
|
|
138
|
+
console.log(`[IFC Cache] Cached ${fileName} (${(fileSize / 1024 / 1024).toFixed(2)}MB)`);
|
|
139
|
+
resolve();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
request.onerror = () => {
|
|
143
|
+
console.error('[IFC Cache] Failed to cache entry:', request.error);
|
|
144
|
+
reject(request.error);
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.warn('[IFC Cache] Cache write failed:', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a cache entry exists
|
|
154
|
+
*/
|
|
155
|
+
export async function hasCached(key: string): Promise<boolean> {
|
|
156
|
+
try {
|
|
157
|
+
const db = await openDatabase();
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
160
|
+
const store = tx.objectStore(STORE_NAME);
|
|
161
|
+
const request = store.count(IDBKeyRange.only(key));
|
|
162
|
+
|
|
163
|
+
request.onsuccess = () => {
|
|
164
|
+
resolve(request.result > 0);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
request.onerror = () => {
|
|
168
|
+
reject(request.error);
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Delete a cache entry
|
|
178
|
+
*/
|
|
179
|
+
export async function deleteCached(key: string): Promise<void> {
|
|
180
|
+
try {
|
|
181
|
+
const db = await openDatabase();
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
184
|
+
const store = tx.objectStore(STORE_NAME);
|
|
185
|
+
const request = store.delete(key);
|
|
186
|
+
|
|
187
|
+
request.onsuccess = () => resolve();
|
|
188
|
+
request.onerror = () => reject(request.error);
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn('[IFC Cache] Failed to delete cache entry:', err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clear all cached models
|
|
197
|
+
*/
|
|
198
|
+
export async function clearCache(): Promise<void> {
|
|
199
|
+
try {
|
|
200
|
+
const db = await openDatabase();
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
203
|
+
const store = tx.objectStore(STORE_NAME);
|
|
204
|
+
const request = store.clear();
|
|
205
|
+
|
|
206
|
+
request.onsuccess = () => {
|
|
207
|
+
console.log('[IFC Cache] Cache cleared');
|
|
208
|
+
resolve();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
request.onerror = () => reject(request.error);
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.warn('[IFC Cache] Failed to clear cache:', err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get cache statistics
|
|
220
|
+
*/
|
|
221
|
+
export async function getCacheStats(): Promise<{
|
|
222
|
+
entryCount: number;
|
|
223
|
+
totalSize: number;
|
|
224
|
+
entries: Array<{ fileName: string; fileSize: number; createdAt: Date }>;
|
|
225
|
+
}> {
|
|
226
|
+
try {
|
|
227
|
+
const db = await openDatabase();
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
230
|
+
const store = tx.objectStore(STORE_NAME);
|
|
231
|
+
const request = store.getAll();
|
|
232
|
+
|
|
233
|
+
request.onsuccess = () => {
|
|
234
|
+
const entries = request.result as CacheEntry[];
|
|
235
|
+
resolve({
|
|
236
|
+
entryCount: entries.length,
|
|
237
|
+
totalSize: entries.reduce((sum, e) => sum + e.buffer.byteLength, 0),
|
|
238
|
+
entries: entries.map((e) => ({
|
|
239
|
+
fileName: e.fileName,
|
|
240
|
+
fileSize: e.fileSize,
|
|
241
|
+
createdAt: new Date(e.createdAt),
|
|
242
|
+
})),
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
request.onerror = () => reject(request.error);
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
return { entryCount: 0, totalSize: 0, entries: [] };
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -307,13 +307,25 @@ export const useViewerStore = create<ViewerState>((set, get) => ({
|
|
|
307
307
|
|
|
308
308
|
// Visibility actions
|
|
309
309
|
hideEntity: (id) => set((state) => {
|
|
310
|
+
// Toggle hide: if already hidden, show it; otherwise hide it
|
|
310
311
|
const newHidden = new Set(state.hiddenEntities);
|
|
311
|
-
newHidden.
|
|
312
|
+
if (newHidden.has(id)) {
|
|
313
|
+
newHidden.delete(id);
|
|
314
|
+
} else {
|
|
315
|
+
newHidden.add(id);
|
|
316
|
+
}
|
|
312
317
|
return { hiddenEntities: newHidden };
|
|
313
318
|
}),
|
|
314
319
|
hideEntities: (ids) => set((state) => {
|
|
320
|
+
// Toggle hide for each entity: if already hidden, show it; otherwise hide it
|
|
315
321
|
const newHidden = new Set(state.hiddenEntities);
|
|
316
|
-
ids.forEach(id =>
|
|
322
|
+
ids.forEach(id => {
|
|
323
|
+
if (newHidden.has(id)) {
|
|
324
|
+
newHidden.delete(id);
|
|
325
|
+
} else {
|
|
326
|
+
newHidden.add(id);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
317
329
|
return { hiddenEntities: newHidden };
|
|
318
330
|
}),
|
|
319
331
|
showEntity: (id) => set((state) => {
|
|
@@ -335,8 +347,47 @@ export const useViewerStore = create<ViewerState>((set, get) => ({
|
|
|
335
347
|
}
|
|
336
348
|
return { hiddenEntities: newHidden };
|
|
337
349
|
}),
|
|
338
|
-
isolateEntity: (id) => set(
|
|
339
|
-
|
|
350
|
+
isolateEntity: (id) => set((state) => {
|
|
351
|
+
// Toggle isolate: if this entity is already the only isolated one, clear isolation
|
|
352
|
+
// Otherwise, isolate it (and unhide it for good UX)
|
|
353
|
+
const isAlreadyIsolated = state.isolatedEntities !== null &&
|
|
354
|
+
state.isolatedEntities.size === 1 &&
|
|
355
|
+
state.isolatedEntities.has(id);
|
|
356
|
+
|
|
357
|
+
if (isAlreadyIsolated) {
|
|
358
|
+
// Toggle off: clear isolation
|
|
359
|
+
return { isolatedEntities: null };
|
|
360
|
+
} else {
|
|
361
|
+
// Toggle on: isolate this entity (and unhide it)
|
|
362
|
+
const newHidden = new Set(state.hiddenEntities);
|
|
363
|
+
newHidden.delete(id);
|
|
364
|
+
return {
|
|
365
|
+
isolatedEntities: new Set([id]),
|
|
366
|
+
hiddenEntities: newHidden,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}),
|
|
370
|
+
isolateEntities: (ids) => set((state) => {
|
|
371
|
+
// Toggle isolate: if these exact entities are already isolated, clear isolation
|
|
372
|
+
// Otherwise, isolate them (and unhide them for good UX)
|
|
373
|
+
const idsSet = new Set(ids);
|
|
374
|
+
const isAlreadyIsolated = state.isolatedEntities !== null &&
|
|
375
|
+
state.isolatedEntities.size === idsSet.size &&
|
|
376
|
+
ids.every(id => state.isolatedEntities!.has(id));
|
|
377
|
+
|
|
378
|
+
if (isAlreadyIsolated) {
|
|
379
|
+
// Toggle off: clear isolation
|
|
380
|
+
return { isolatedEntities: null };
|
|
381
|
+
} else {
|
|
382
|
+
// Toggle on: isolate these entities (and unhide them)
|
|
383
|
+
const newHidden = new Set(state.hiddenEntities);
|
|
384
|
+
ids.forEach(id => newHidden.delete(id));
|
|
385
|
+
return {
|
|
386
|
+
isolatedEntities: idsSet,
|
|
387
|
+
hiddenEntities: newHidden,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
340
391
|
clearIsolation: () => set({ isolatedEntities: null }),
|
|
341
392
|
showAll: () => set({ hiddenEntities: new Set(), isolatedEntities: null }),
|
|
342
393
|
isEntityVisible: (id) => {
|
package/tsconfig.json
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"@ifc-lite/parser": ["../../packages/parser/src"],
|
|
10
10
|
"@ifc-lite/geometry": ["../../packages/geometry/src"],
|
|
11
11
|
"@ifc-lite/renderer": ["../../packages/renderer/src"],
|
|
12
|
-
"@ifc-lite/query": ["../../packages/query/src"]
|
|
12
|
+
"@ifc-lite/query": ["../../packages/query/src"],
|
|
13
|
+
"@ifc-lite/spatial": ["../../packages/spatial/src"],
|
|
14
|
+
"@ifc-lite/data": ["../../packages/data/src"],
|
|
15
|
+
"@ifc-lite/export": ["../../packages/export/src"],
|
|
16
|
+
"@ifc-lite/cache": ["../../packages/cache/src"]
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"]
|