@ifc-lite/viewer 1.14.0 → 1.14.1
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/CHANGELOG.md +24 -0
- package/dist/assets/{Arrow.dom-CNguvlQi.js → Arrow.dom-HLSMJR_v.js} +1 -1
- package/dist/assets/{browser-D6lgLpkA.js → browser-Ch0OnmZN.js} +1 -1
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/{index-UaDsJsCR.js → index-DJbbSLF9.js} +19897 -19545
- package/dist/assets/{index-BMwpw264.js → index-JPFMj8C9.js} +4 -4
- package/dist/assets/{native-bridge-DqELq4X0.js → native-bridge-BzC7HkDs.js} +1 -1
- package/dist/assets/{wasm-bridge-CVWvHlfH.js → wasm-bridge-B_7dPwOa.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +19 -19
- package/src/components/viewer/HierarchyPanel.tsx +7 -1
- package/src/components/viewer/MainToolbar.tsx +51 -18
- package/src/components/viewer/Viewport.tsx +5 -1
- package/src/components/viewer/ViewportContainer.tsx +59 -37
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +8 -4
- package/src/components/viewer/properties/BsddCard.tsx +2 -2
- package/src/components/viewer/useGeometryStreaming.ts +189 -55
- package/src/components/viewer/useMouseControls.ts +55 -14
- package/src/components/viewer/useTouchControls.ts +2 -0
- package/src/hooks/useIfc.ts +19 -1
- package/src/hooks/useIfcCache.ts +6 -1
- package/src/hooks/useIfcFederation.ts +16 -1
- package/src/hooks/useIfcLoader.ts +16 -4
- package/src/store/slices/dataSlice.ts +9 -4
- package/src/utils/localParsingUtils.ts +3 -1
- package/tsconfig.json +12 -1
- package/dist/assets/ifc-lite_bg-B6s-pcv0.wasm +0 -0
|
@@ -15,6 +15,9 @@ export interface UseGeometryStreamingParams {
|
|
|
15
15
|
rendererRef: MutableRefObject<Renderer | null>;
|
|
16
16
|
isInitialized: boolean;
|
|
17
17
|
geometry: MeshData[] | null;
|
|
18
|
+
/** Monotonic counter — triggers the streaming effect even when the geometry
|
|
19
|
+
* array reference is stable (incremental filtering reuses the same array). */
|
|
20
|
+
geometryVersion?: number;
|
|
18
21
|
coordinateInfo?: CoordinateInfo;
|
|
19
22
|
isStreaming: boolean;
|
|
20
23
|
geometryBoundsRef: MutableRefObject<{ min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } }>;
|
|
@@ -31,6 +34,7 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
31
34
|
rendererRef,
|
|
32
35
|
isInitialized,
|
|
33
36
|
geometry,
|
|
37
|
+
geometryVersion,
|
|
34
38
|
coordinateInfo,
|
|
35
39
|
isStreaming,
|
|
36
40
|
geometryBoundsRef,
|
|
@@ -49,6 +53,11 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
49
53
|
const cameraFittedRef = useRef<boolean>(false);
|
|
50
54
|
const finalBoundsRefittedRef = useRef<boolean>(false); // Track if we've refitted after streaming
|
|
51
55
|
|
|
56
|
+
// Track camera state after initial fit to detect user interaction during streaming.
|
|
57
|
+
// If user orbits/pans/zooms during streaming, we preserve their position at completion
|
|
58
|
+
// instead of snapping back to the computed view. Bounds still update for "home" etc.
|
|
59
|
+
const cameraStateAfterFitRef = useRef<{ px: number; py: number; pz: number; tx: number; ty: number; tz: number } | null>(null);
|
|
60
|
+
|
|
52
61
|
// Render throttling during streaming
|
|
53
62
|
const lastStreamRenderTimeRef = useRef<number>(0);
|
|
54
63
|
const STREAM_RENDER_THROTTLE_MS = 200; // Render at most every 200ms during streaming
|
|
@@ -65,6 +74,7 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
65
74
|
processedMeshIdsRef.current.clear();
|
|
66
75
|
cameraFittedRef.current = false;
|
|
67
76
|
finalBoundsRefittedRef.current = false;
|
|
77
|
+
cameraStateAfterFitRef.current = null;
|
|
68
78
|
// Clear scene if renderer is ready
|
|
69
79
|
if (renderer && isInitialized) {
|
|
70
80
|
renderer.getScene().clear();
|
|
@@ -110,6 +120,7 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
110
120
|
processedMeshIdsRef.current.clear();
|
|
111
121
|
cameraFittedRef.current = false;
|
|
112
122
|
finalBoundsRefittedRef.current = false;
|
|
123
|
+
cameraStateAfterFitRef.current = null;
|
|
113
124
|
lastGeometryLengthRef.current = 0;
|
|
114
125
|
lastGeometryRef.current = geometry;
|
|
115
126
|
// Reset camera state (clear orbit pivot, stop inertia, cancel animations)
|
|
@@ -139,6 +150,7 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
139
150
|
processedMeshIdsRef.current.clear();
|
|
140
151
|
cameraFittedRef.current = false;
|
|
141
152
|
finalBoundsRefittedRef.current = false;
|
|
153
|
+
cameraStateAfterFitRef.current = null;
|
|
142
154
|
lastGeometryLengthRef.current = 0;
|
|
143
155
|
lastGeometryRef.current = geometry;
|
|
144
156
|
// Reset camera state
|
|
@@ -150,45 +162,86 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
150
162
|
};
|
|
151
163
|
}
|
|
152
164
|
} else if (currentLength === lastLength) {
|
|
153
|
-
// No geometry change
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
// No geometry change — update bounds when streaming completes.
|
|
166
|
+
// Two behaviours depending on user camera interaction:
|
|
167
|
+
// • User hasn't touched the camera → refit to final bounds (fixes models
|
|
168
|
+
// whose first-batch bounds were too tight / off-centre).
|
|
169
|
+
// • User HAS orbited/panned/zoomed → preserve their position; just store
|
|
170
|
+
// the final bounds so "Home" / zoom-to-fit still work correctly.
|
|
171
|
+
if (cameraFittedRef.current && !isStreaming && !finalBoundsRefittedRef.current) {
|
|
172
|
+
// Compute EXACT bounds from all geometry vertices.
|
|
173
|
+
// coordinateInfo.shiftedBounds uses fast vertex-sampling (first+last vertex per mesh)
|
|
174
|
+
// which can miss the true extremes — e.g., a 22-storey building whose highest vertices
|
|
175
|
+
// are mid-buffer gets truncated bounds. Scanning all vertices here is ~15 ms for 3 M
|
|
176
|
+
// vertices and only runs once at streaming completion.
|
|
177
|
+
const MAX_VALID_COORD = 10000;
|
|
178
|
+
const exactBounds = {
|
|
179
|
+
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
180
|
+
max: { x: -Infinity, y: -Infinity, z: -Infinity },
|
|
181
|
+
};
|
|
182
|
+
for (let gi = 0; gi < geometry.length; gi++) {
|
|
183
|
+
const positions = geometry[gi].positions;
|
|
184
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
185
|
+
const x = positions[i];
|
|
186
|
+
const y = positions[i + 1];
|
|
187
|
+
const z = positions[i + 2];
|
|
188
|
+
if (Math.abs(x) < MAX_VALID_COORD && Math.abs(y) < MAX_VALID_COORD && Math.abs(z) < MAX_VALID_COORD) {
|
|
189
|
+
if (x < exactBounds.min.x) exactBounds.min.x = x;
|
|
190
|
+
if (y < exactBounds.min.y) exactBounds.min.y = y;
|
|
191
|
+
if (z < exactBounds.min.z) exactBounds.min.z = z;
|
|
192
|
+
if (x > exactBounds.max.x) exactBounds.max.x = x;
|
|
193
|
+
if (y > exactBounds.max.y) exactBounds.max.y = y;
|
|
194
|
+
if (z > exactBounds.max.z) exactBounds.max.z = z;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const exactMaxSize = Math.max(
|
|
200
|
+
exactBounds.max.x - exactBounds.min.x,
|
|
201
|
+
exactBounds.max.y - exactBounds.min.y,
|
|
202
|
+
exactBounds.max.z - exactBounds.min.z
|
|
160
203
|
);
|
|
161
204
|
|
|
162
|
-
if (
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Refit camera if bounds expanded significantly (>10% larger)
|
|
176
|
-
// This handles skyscrapers where upper floors arrive in later batches
|
|
177
|
-
const boundsExpanded = newMaxSize > oldMaxSize * 1.1;
|
|
178
|
-
|
|
179
|
-
if (boundsExpanded) {
|
|
180
|
-
renderer.getCamera().fitToBounds(shiftedBounds.min, shiftedBounds.max);
|
|
181
|
-
}
|
|
205
|
+
if (exactBounds.min.x !== Infinity && exactMaxSize > 0 && Number.isFinite(exactMaxSize)) {
|
|
206
|
+
// Detect whether the user moved the camera during streaming
|
|
207
|
+
const snap = cameraStateAfterFitRef.current;
|
|
208
|
+
let userMovedCamera = false;
|
|
209
|
+
if (snap) {
|
|
210
|
+
const pos = renderer.getCamera().getPosition();
|
|
211
|
+
const tgt = renderer.getCamera().getTarget();
|
|
212
|
+
const EPS = 0.5; // half a metre — ignores floating-point jitter
|
|
213
|
+
userMovedCamera =
|
|
214
|
+
Math.abs(pos.x - snap.px) > EPS || Math.abs(pos.y - snap.py) > EPS || Math.abs(pos.z - snap.pz) > EPS ||
|
|
215
|
+
Math.abs(tgt.x - snap.tx) > EPS || Math.abs(tgt.y - snap.ty) > EPS || Math.abs(tgt.z - snap.tz) > EPS;
|
|
182
216
|
}
|
|
183
217
|
|
|
184
|
-
|
|
185
|
-
|
|
218
|
+
if (!userMovedCamera) {
|
|
219
|
+
// User hasn't interacted — refit to exact full bounds
|
|
220
|
+
renderer.getCamera().fitToBounds(exactBounds.min, exactBounds.max);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Always update stored bounds for Home / zoom-to-fit
|
|
224
|
+
geometryBoundsRef.current = { min: { ...exactBounds.min }, max: { ...exactBounds.max } };
|
|
186
225
|
finalBoundsRefittedRef.current = true;
|
|
187
226
|
}
|
|
188
227
|
}
|
|
189
228
|
return;
|
|
190
229
|
}
|
|
191
230
|
|
|
231
|
+
// Detect post-streaming type visibility toggle: geometry grew while NOT streaming.
|
|
232
|
+
// The filtered array was rebuilt from scratch (spaces/openings/site toggled ON),
|
|
233
|
+
// with new meshes interleaved throughout — not just appended at the end.
|
|
234
|
+
// We must clear the scene and re-add ALL meshes.
|
|
235
|
+
// Distinguish from streaming-completion (prevIsStreaming→!isStreaming) where new
|
|
236
|
+
// meshes ARE appended at the end and scene should NOT be cleared.
|
|
237
|
+
if (isIncremental && !isStreaming && !prevIsStreamingRef.current) {
|
|
238
|
+
scene.clear();
|
|
239
|
+
processedMeshIdsRef.current.clear();
|
|
240
|
+
// Don't reset camera or bounds — user just toggled visibility
|
|
241
|
+
lastGeometryLengthRef.current = 0;
|
|
242
|
+
lastGeometryRef.current = geometry;
|
|
243
|
+
}
|
|
244
|
+
|
|
192
245
|
// For incremental batches: update reference and continue to add new meshes
|
|
193
246
|
if (isIncremental) {
|
|
194
247
|
lastGeometryRef.current = geometry;
|
|
@@ -196,28 +249,37 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
196
249
|
lastGeometryRef.current = geometry;
|
|
197
250
|
}
|
|
198
251
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
252
|
+
// PERF: During streaming, new meshes are ALWAYS appended at the end.
|
|
253
|
+
// Skip the compound key dedup (208K string allocations + Set lookups)
|
|
254
|
+
// and array copy (.slice()). Use index-based iteration directly.
|
|
255
|
+
//
|
|
256
|
+
// FIX: Use fast path for incremental appends too (not just streaming).
|
|
257
|
+
// When streaming completes, isStreaming becomes false in the same render as the
|
|
258
|
+
// final appendGeometryBatch. The slow path would re-add ALL meshes because
|
|
259
|
+
// processedMeshIdsRef was never populated during streaming, causing double geometry.
|
|
260
|
+
// For visibility toggles, lastGeometryLengthRef was reset to 0 above, so the
|
|
261
|
+
// fast path naturally starts from 0 (adding ALL meshes after scene.clear).
|
|
262
|
+
let newMeshes: MeshData[];
|
|
263
|
+
if (isStreaming || isIncremental) {
|
|
264
|
+
// Fast path: iterate from lastLength directly
|
|
265
|
+
// During streaming: new meshes appended at end, start = previous length
|
|
266
|
+
// After visibility toggle: scene was cleared, start = 0, adds everything
|
|
267
|
+
const start = lastGeometryLengthRef.current;
|
|
268
|
+
newMeshes = [];
|
|
269
|
+
for (let i = start; i < geometry.length; i++) {
|
|
270
|
+
newMeshes.push(geometry[i]);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Slow path: scan entire array for unprocessed meshes
|
|
274
|
+
// Only used when array was fully rebuilt (not incremental)
|
|
275
|
+
newMeshes = [];
|
|
276
|
+
for (let i = 0; i < geometry.length; i++) {
|
|
277
|
+
const meshData = geometry[i];
|
|
278
|
+
const compoundKey = `${meshData.expressId}:${i}`;
|
|
279
|
+
if (!processedMeshIdsRef.current.has(compoundKey)) {
|
|
280
|
+
newMeshes.push(meshData);
|
|
281
|
+
processedMeshIdsRef.current.add(compoundKey);
|
|
282
|
+
}
|
|
221
283
|
}
|
|
222
284
|
}
|
|
223
285
|
|
|
@@ -294,6 +356,10 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
294
356
|
renderer.getCamera().fitToBounds(shiftedBounds.min, shiftedBounds.max);
|
|
295
357
|
geometryBoundsRef.current = { min: { ...shiftedBounds.min }, max: { ...shiftedBounds.max } };
|
|
296
358
|
cameraFittedRef.current = true;
|
|
359
|
+
// Snapshot camera state so we can detect user interaction during streaming
|
|
360
|
+
const pos = renderer.getCamera().getPosition();
|
|
361
|
+
const tgt = renderer.getCamera().getTarget();
|
|
362
|
+
cameraStateAfterFitRef.current = { px: pos.x, py: pos.y, pz: pos.z, tx: tgt.x, ty: tgt.y, tz: tgt.z };
|
|
297
363
|
}
|
|
298
364
|
} else if (!cameraFittedRef.current && geometry.length > 0 && !isStreaming) {
|
|
299
365
|
// Fallback: calculate bounds from geometry array (only when streaming is complete)
|
|
@@ -336,6 +402,9 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
336
402
|
renderer.getCamera().fitToBounds(fallbackBounds.min, fallbackBounds.max);
|
|
337
403
|
geometryBoundsRef.current = fallbackBounds;
|
|
338
404
|
cameraFittedRef.current = true;
|
|
405
|
+
const pos = renderer.getCamera().getPosition();
|
|
406
|
+
const tgt = renderer.getCamera().getTarget();
|
|
407
|
+
cameraStateAfterFitRef.current = { px: pos.x, py: pos.y, pz: pos.z, tx: tgt.x, ty: tgt.y, tz: tgt.z };
|
|
339
408
|
}
|
|
340
409
|
}
|
|
341
410
|
|
|
@@ -351,10 +420,12 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
351
420
|
const shouldRender = !isStreaming || timeSinceLastRender >= STREAM_RENDER_THROTTLE_MS;
|
|
352
421
|
|
|
353
422
|
if (shouldRender) {
|
|
354
|
-
renderer.render(
|
|
423
|
+
renderer.render({
|
|
424
|
+
clearColor: clearColorRef.current,
|
|
425
|
+
});
|
|
355
426
|
lastStreamRenderTimeRef.current = now;
|
|
356
427
|
}
|
|
357
|
-
}, [geometry, coordinateInfo, isInitialized, isStreaming]);
|
|
428
|
+
}, [geometry, geometryVersion, coordinateInfo, isInitialized, isStreaming]);
|
|
358
429
|
|
|
359
430
|
// Force render when streaming completes (progress goes from <100% to 100% or null)
|
|
360
431
|
const prevIsStreamingRef = useRef(isStreaming);
|
|
@@ -368,12 +439,75 @@ export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
|
|
|
368
439
|
const pipeline = renderer.getPipeline();
|
|
369
440
|
const scene = renderer.getScene();
|
|
370
441
|
|
|
371
|
-
// Finalize streaming: destroy temporary fragments and do one O(N) full merge
|
|
442
|
+
// Finalize streaming: destroy temporary fragments and do one O(N) full merge.
|
|
443
|
+
// Must run synchronously BEFORE pendingMeshColorUpdates effect — otherwise
|
|
444
|
+
// fragment batches with stale colors render alongside new proper batches.
|
|
372
445
|
if (device && pipeline) {
|
|
373
446
|
scene.finalizeStreaming(device, pipeline);
|
|
374
447
|
}
|
|
375
448
|
|
|
376
|
-
|
|
449
|
+
// Compute exact bounds and refit camera if not already done.
|
|
450
|
+
// This MUST happen here rather than only in the main geometry effect's
|
|
451
|
+
// `currentLength === lastLength` branch, because React may batch the
|
|
452
|
+
// final appendGeometryBatch (geometry grows) and setIsStreaming(false)
|
|
453
|
+
// into the SAME render — making the main effect take the incremental
|
|
454
|
+
// `currentLength > lastLength` path and skip the bounds refit entirely.
|
|
455
|
+
// This effect reliably fires on the isStreaming true→false transition.
|
|
456
|
+
if (cameraFittedRef.current && !finalBoundsRefittedRef.current && geometry && geometry.length > 0) {
|
|
457
|
+
const MAX_VALID_COORD = 10000;
|
|
458
|
+
const exactBounds = {
|
|
459
|
+
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
460
|
+
max: { x: -Infinity, y: -Infinity, z: -Infinity },
|
|
461
|
+
};
|
|
462
|
+
for (let gi = 0; gi < geometry.length; gi++) {
|
|
463
|
+
const positions = geometry[gi].positions;
|
|
464
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
465
|
+
const x = positions[i];
|
|
466
|
+
const y = positions[i + 1];
|
|
467
|
+
const z = positions[i + 2];
|
|
468
|
+
if (Math.abs(x) < MAX_VALID_COORD && Math.abs(y) < MAX_VALID_COORD && Math.abs(z) < MAX_VALID_COORD) {
|
|
469
|
+
if (x < exactBounds.min.x) exactBounds.min.x = x;
|
|
470
|
+
if (y < exactBounds.min.y) exactBounds.min.y = y;
|
|
471
|
+
if (z < exactBounds.min.z) exactBounds.min.z = z;
|
|
472
|
+
if (x > exactBounds.max.x) exactBounds.max.x = x;
|
|
473
|
+
if (y > exactBounds.max.y) exactBounds.max.y = y;
|
|
474
|
+
if (z > exactBounds.max.z) exactBounds.max.z = z;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const exactMaxSize = Math.max(
|
|
480
|
+
exactBounds.max.x - exactBounds.min.x,
|
|
481
|
+
exactBounds.max.y - exactBounds.min.y,
|
|
482
|
+
exactBounds.max.z - exactBounds.min.z
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (exactBounds.min.x !== Infinity && exactMaxSize > 0 && Number.isFinite(exactMaxSize)) {
|
|
486
|
+
// Detect whether the user moved the camera during streaming
|
|
487
|
+
const snap = cameraStateAfterFitRef.current;
|
|
488
|
+
let userMovedCamera = false;
|
|
489
|
+
if (snap) {
|
|
490
|
+
const pos = renderer.getCamera().getPosition();
|
|
491
|
+
const tgt = renderer.getCamera().getTarget();
|
|
492
|
+
const EPS = 0.5; // half a metre — ignores floating-point jitter
|
|
493
|
+
userMovedCamera =
|
|
494
|
+
Math.abs(pos.x - snap.px) > EPS || Math.abs(pos.y - snap.py) > EPS || Math.abs(pos.z - snap.pz) > EPS ||
|
|
495
|
+
Math.abs(tgt.x - snap.tx) > EPS || Math.abs(tgt.y - snap.ty) > EPS || Math.abs(tgt.z - snap.tz) > EPS;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!userMovedCamera) {
|
|
499
|
+
renderer.getCamera().fitToBounds(exactBounds.min, exactBounds.max);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Always update stored bounds for Home / zoom-to-fit
|
|
503
|
+
geometryBoundsRef.current = { min: { ...exactBounds.min }, max: { ...exactBounds.max } };
|
|
504
|
+
finalBoundsRefittedRef.current = true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
renderer.render({
|
|
509
|
+
clearColor: clearColorRef.current,
|
|
510
|
+
});
|
|
377
511
|
lastStreamRenderTimeRef.current = Date.now();
|
|
378
512
|
}
|
|
379
513
|
prevIsStreamingRef.current = isStreaming;
|
|
@@ -716,6 +716,7 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
716
716
|
selectedId: selectedEntityIdRef.current,
|
|
717
717
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
718
718
|
clearColor: clearColorRef.current,
|
|
719
|
+
isInteracting: true,
|
|
719
720
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
720
721
|
...sectionPlaneRef.current,
|
|
721
722
|
min: sectionRangeRef.current?.min,
|
|
@@ -727,7 +728,9 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
727
728
|
calculateScale();
|
|
728
729
|
} else if (!renderPendingRef.current) {
|
|
729
730
|
// Schedule a final render for when throttle expires
|
|
730
|
-
//
|
|
731
|
+
// IMPORTANT: Keep isInteracting: true during drag to prevent flickering
|
|
732
|
+
// caused by post-processing toggling on/off between throttled frames.
|
|
733
|
+
// Post-processing is restored on mouseup (non-interacting render).
|
|
731
734
|
renderPendingRef.current = true;
|
|
732
735
|
requestAnimationFrame(() => {
|
|
733
736
|
renderPendingRef.current = false;
|
|
@@ -737,6 +740,7 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
737
740
|
selectedId: selectedEntityIdRef.current,
|
|
738
741
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
739
742
|
clearColor: clearColorRef.current,
|
|
743
|
+
isInteracting: true,
|
|
740
744
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
741
745
|
...sectionPlaneRef.current,
|
|
742
746
|
min: sectionRangeRef.current?.min,
|
|
@@ -890,18 +894,56 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
890
894
|
const mouseX = e.clientX - rect.left;
|
|
891
895
|
const mouseY = e.clientY - rect.top;
|
|
892
896
|
camera.zoom(e.deltaY, false, mouseX, mouseY, canvas.width, canvas.height);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
897
|
+
|
|
898
|
+
// PERFORMANCE: Adaptive throttle for wheel zoom (same as orbit)
|
|
899
|
+
// Without this, every wheel event triggers a synchronous render —
|
|
900
|
+
// wheel events fire at 60-120Hz which overwhelms the GPU on large models.
|
|
901
|
+
const meshCount = geometryRef.current?.length ?? 0;
|
|
902
|
+
const throttleMs = meshCount > 50000 ? RENDER_THROTTLE_MS_HUGE
|
|
903
|
+
: meshCount > 10000 ? RENDER_THROTTLE_MS_LARGE
|
|
904
|
+
: RENDER_THROTTLE_MS_SMALL;
|
|
905
|
+
|
|
906
|
+
const now = performance.now();
|
|
907
|
+
if (now - lastRenderTimeRef.current >= throttleMs) {
|
|
908
|
+
lastRenderTimeRef.current = now;
|
|
909
|
+
renderer.render({
|
|
910
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
911
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
912
|
+
selectedId: selectedEntityIdRef.current,
|
|
913
|
+
selectedModelIndex: selectedModelIndexRef.current,
|
|
914
|
+
clearColor: clearColorRef.current,
|
|
915
|
+
isInteracting: true,
|
|
916
|
+
sectionPlane: activeToolRef.current === 'section' ? {
|
|
917
|
+
...sectionPlaneRef.current,
|
|
918
|
+
min: sectionRangeRef.current?.min,
|
|
919
|
+
max: sectionRangeRef.current?.max,
|
|
920
|
+
} : undefined,
|
|
921
|
+
});
|
|
922
|
+
calculateScale();
|
|
923
|
+
} else if (!renderPendingRef.current) {
|
|
924
|
+
// Schedule a final render to ensure we always render the last zoom position
|
|
925
|
+
// IMPORTANT: Keep isInteracting: true to prevent flickering from post-processing
|
|
926
|
+
// toggling. Post-processing is restored by the zoom idle timer below.
|
|
927
|
+
renderPendingRef.current = true;
|
|
928
|
+
requestAnimationFrame(() => {
|
|
929
|
+
renderPendingRef.current = false;
|
|
930
|
+
renderer.render({
|
|
931
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
932
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
933
|
+
selectedId: selectedEntityIdRef.current,
|
|
934
|
+
selectedModelIndex: selectedModelIndexRef.current,
|
|
935
|
+
clearColor: clearColorRef.current,
|
|
936
|
+
isInteracting: true,
|
|
937
|
+
sectionPlane: activeToolRef.current === 'section' ? {
|
|
938
|
+
...sectionPlaneRef.current,
|
|
939
|
+
min: sectionRangeRef.current?.min,
|
|
940
|
+
max: sectionRangeRef.current?.max,
|
|
941
|
+
} : undefined,
|
|
942
|
+
});
|
|
943
|
+
calculateScale();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
905
947
|
// Update measurement screen coordinates immediately during zoom (only in measure mode)
|
|
906
948
|
if (activeToolRef.current === 'measure') {
|
|
907
949
|
if (hasPendingMeasurements()) {
|
|
@@ -921,7 +963,6 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
921
963
|
};
|
|
922
964
|
}
|
|
923
965
|
}
|
|
924
|
-
calculateScale();
|
|
925
966
|
};
|
|
926
967
|
|
|
927
968
|
// Click handling
|
|
@@ -150,6 +150,7 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
150
150
|
selectedId: selectedEntityIdRef.current,
|
|
151
151
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
152
152
|
clearColor: clearColorRef.current,
|
|
153
|
+
isInteracting: true,
|
|
153
154
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
154
155
|
...sectionPlaneRef.current,
|
|
155
156
|
min: sectionRangeRef.current?.min,
|
|
@@ -179,6 +180,7 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
179
180
|
selectedId: selectedEntityIdRef.current,
|
|
180
181
|
selectedModelIndex: selectedModelIndexRef.current,
|
|
181
182
|
clearColor: clearColorRef.current,
|
|
183
|
+
isInteracting: true,
|
|
182
184
|
sectionPlane: activeToolRef.current === 'section' ? {
|
|
183
185
|
...sectionPlaneRef.current,
|
|
184
186
|
min: sectionRangeRef.current?.min,
|
package/src/hooks/useIfc.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useMemo, useRef } from 'react';
|
|
16
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
16
17
|
import { useViewerStore } from '../store.js';
|
|
17
18
|
import { IfcQuery } from '@ifc-lite/query';
|
|
18
19
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
@@ -41,7 +42,24 @@ export function useIfc() {
|
|
|
41
42
|
hasModels,
|
|
42
43
|
// Federation Registry helpers
|
|
43
44
|
toGlobalId,
|
|
44
|
-
} = useViewerStore()
|
|
45
|
+
} = useViewerStore(useShallow((s) => ({
|
|
46
|
+
loading: s.loading,
|
|
47
|
+
progress: s.progress,
|
|
48
|
+
error: s.error,
|
|
49
|
+
ifcDataStore: s.ifcDataStore,
|
|
50
|
+
geometryResult: s.geometryResult,
|
|
51
|
+
models: s.models,
|
|
52
|
+
activeModelId: s.activeModelId,
|
|
53
|
+
clearAllModels: s.clearAllModels,
|
|
54
|
+
setActiveModel: s.setActiveModel,
|
|
55
|
+
setModelVisibility: s.setModelVisibility,
|
|
56
|
+
setModelCollapsed: s.setModelCollapsed,
|
|
57
|
+
getModel: s.getModel,
|
|
58
|
+
getActiveModel: s.getActiveModel,
|
|
59
|
+
getAllVisibleModels: s.getAllVisibleModels,
|
|
60
|
+
hasModels: s.hasModels,
|
|
61
|
+
toGlobalId: s.toGlobalId,
|
|
62
|
+
})));
|
|
45
63
|
|
|
46
64
|
// Track if we've already logged for this ifcDataStore
|
|
47
65
|
const lastLoggedDataStoreRef = useRef<typeof ifcDataStore>(null);
|
package/src/hooks/useIfcCache.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { SpatialHierarchyBuilder, extractLengthUnitScale, type IfcDataStore } fr
|
|
|
20
20
|
import { buildSpatialIndex } from '@ifc-lite/spatial';
|
|
21
21
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
22
22
|
|
|
23
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
23
24
|
import { useViewerStore } from '../store.js';
|
|
24
25
|
import { getCached, setCached, deleteCached, type CacheResult } from '../services/cacheService.js';
|
|
25
26
|
import { rebuildSpatialHierarchy, rebuildOnDemandMaps } from '../utils/spatialHierarchy.js';
|
|
@@ -66,7 +67,11 @@ export function useIfcCache() {
|
|
|
66
67
|
setProgress,
|
|
67
68
|
setIfcDataStore,
|
|
68
69
|
setGeometryResult,
|
|
69
|
-
} = useViewerStore()
|
|
70
|
+
} = useViewerStore(useShallow((s) => ({
|
|
71
|
+
setProgress: s.setProgress,
|
|
72
|
+
setIfcDataStore: s.setIfcDataStore,
|
|
73
|
+
setGeometryResult: s.setGeometryResult,
|
|
74
|
+
})));
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
77
|
* Load from binary cache - INSTANT load for maximum speed
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useCallback } from 'react';
|
|
14
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
14
15
|
import { useViewerStore, type FederatedModel, type SchemaVersion } from '../store.js';
|
|
15
16
|
import { IfcParser, detectFormat, parseIfcx, parseFederatedIfcx, type IfcDataStore, type FederatedIfcxParseResult } from '@ifc-lite/parser';
|
|
16
17
|
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
@@ -99,7 +100,21 @@ export function useIfcFederation() {
|
|
|
99
100
|
registerModelOffset,
|
|
100
101
|
fromGlobalId,
|
|
101
102
|
findModelForGlobalId,
|
|
102
|
-
} = useViewerStore()
|
|
103
|
+
} = useViewerStore(useShallow((s) => ({
|
|
104
|
+
setLoading: s.setLoading,
|
|
105
|
+
setError: s.setError,
|
|
106
|
+
setProgress: s.setProgress,
|
|
107
|
+
setIfcDataStore: s.setIfcDataStore,
|
|
108
|
+
setGeometryResult: s.setGeometryResult,
|
|
109
|
+
addModel: s.addModel,
|
|
110
|
+
removeModel: s.removeModel,
|
|
111
|
+
clearAllModels: s.clearAllModels,
|
|
112
|
+
getModel: s.getModel,
|
|
113
|
+
hasModels: s.hasModels,
|
|
114
|
+
registerModelOffset: s.registerModelOffset,
|
|
115
|
+
fromGlobalId: s.fromGlobalId,
|
|
116
|
+
findModelForGlobalId: s.findModelForGlobalId,
|
|
117
|
+
})));
|
|
103
118
|
|
|
104
119
|
/**
|
|
105
120
|
* Add a model to the federation (multi-model support)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { useCallback } from 'react';
|
|
14
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
14
15
|
import { useViewerStore } from '../store.js';
|
|
15
16
|
import { IfcParser, detectFormat, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
|
|
16
17
|
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
@@ -77,7 +78,16 @@ export function useIfcLoader() {
|
|
|
77
78
|
appendGeometryBatch,
|
|
78
79
|
updateMeshColors,
|
|
79
80
|
updateCoordinateInfo,
|
|
80
|
-
} = useViewerStore()
|
|
81
|
+
} = useViewerStore(useShallow((s) => ({
|
|
82
|
+
setLoading: s.setLoading,
|
|
83
|
+
setError: s.setError,
|
|
84
|
+
setProgress: s.setProgress,
|
|
85
|
+
setIfcDataStore: s.setIfcDataStore,
|
|
86
|
+
setGeometryResult: s.setGeometryResult,
|
|
87
|
+
appendGeometryBatch: s.appendGeometryBatch,
|
|
88
|
+
updateMeshColors: s.updateMeshColors,
|
|
89
|
+
updateCoordinateInfo: s.updateCoordinateInfo,
|
|
90
|
+
})));
|
|
81
91
|
|
|
82
92
|
// Cache operations from extracted hook
|
|
83
93
|
const { loadFromCache, saveToCache } = useIfcCache();
|
|
@@ -450,9 +460,11 @@ export function useIfcLoader() {
|
|
|
450
460
|
|
|
451
461
|
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
452
462
|
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
463
|
+
// PERF: Defer data model parsing to next macrotask so the browser
|
|
464
|
+
// can paint the streaming-complete state first. parseColumnar()
|
|
465
|
+
// synchronously calls scanEntitiesFast() which blocks the main
|
|
466
|
+
// thread for ~7s on large files (487MB → 8.4M entities).
|
|
467
|
+
setTimeout(startDataModelParsing, 0);
|
|
456
468
|
|
|
457
469
|
// Apply all accumulated color updates in a single store update
|
|
458
470
|
// instead of one updateMeshColors() call per colorUpdate event.
|
|
@@ -81,13 +81,18 @@ export const createDataSlice: StateCreator<DataSlice, [], [], DataSlice> = (set)
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// PERF FIX: Push into existing array — O(batch_size) instead of O(total).
|
|
85
|
+
// The old [...old, ...new] spread copied ALL accumulated meshes every batch,
|
|
86
|
+
// causing O(N²) total work (e.g., 176K meshes × 350 batches = 31M copies).
|
|
87
|
+
// Zustand detects changes via the new geometryResult object reference below.
|
|
88
|
+
const existing = state.geometryResult.meshes;
|
|
89
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
90
|
+
existing.push(meshes[i]);
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
return {
|
|
88
94
|
geometryResult: {
|
|
89
95
|
...state.geometryResult,
|
|
90
|
-
meshes: [...state.geometryResult.meshes, ...meshes],
|
|
91
96
|
totalTriangles: state.geometryResult.totalTriangles + batchTriangles,
|
|
92
97
|
totalVertices: state.geometryResult.totalVertices + batchVertices,
|
|
93
98
|
coordinateInfo: coordinateInfo || state.geometryResult.coordinateInfo,
|
|
@@ -171,7 +171,9 @@ export function createCoordinateInfo(
|
|
|
171
171
|
* @returns Render interval in milliseconds
|
|
172
172
|
*/
|
|
173
173
|
export function getRenderIntervalMs(fileSizeMB: number): number {
|
|
174
|
-
if (fileSizeMB >
|
|
174
|
+
if (fileSizeMB > 300) {
|
|
175
|
+
return 500; // Very large files: 2 updates/sec (fewer GPU fragment creations)
|
|
176
|
+
} else if (fileSizeMB > 100) {
|
|
175
177
|
return 200; // Huge files: 5 updates/sec
|
|
176
178
|
} else if (fileSizeMB > 50) {
|
|
177
179
|
return 100; // Large files: 10 updates/sec
|
package/tsconfig.json
CHANGED
|
@@ -15,7 +15,18 @@
|
|
|
15
15
|
"@ifc-lite/export": ["../../packages/export/src"],
|
|
16
16
|
"@ifc-lite/cache": ["../../packages/cache/src"],
|
|
17
17
|
"@ifc-lite/sdk": ["../../packages/sdk/src"],
|
|
18
|
-
"@ifc-lite/lens": ["../../packages/lens/src"]
|
|
18
|
+
"@ifc-lite/lens": ["../../packages/lens/src"],
|
|
19
|
+
"@ifc-lite/bcf": ["../../packages/bcf/src"],
|
|
20
|
+
"@ifc-lite/mutations": ["../../packages/mutations/src"],
|
|
21
|
+
"@ifc-lite/drawing-2d": ["../../packages/drawing-2d/src"],
|
|
22
|
+
"@ifc-lite/server-client": ["../../packages/server-client/src"],
|
|
23
|
+
"@ifc-lite/ifcx": ["../../packages/ifcx/src"],
|
|
24
|
+
"@ifc-lite/create": ["../../packages/create/src"],
|
|
25
|
+
"@ifc-lite/ids": ["../../packages/ids/src"],
|
|
26
|
+
"@ifc-lite/encoding": ["../../packages/encoding/src"],
|
|
27
|
+
"@ifc-lite/lists": ["../../packages/lists/src"],
|
|
28
|
+
"@ifc-lite/sandbox": ["../../packages/sandbox/src"],
|
|
29
|
+
"@ifc-lite/sandbox/schema": ["../../packages/sandbox/src/bridge-schema"]
|
|
19
30
|
}
|
|
20
31
|
},
|
|
21
32
|
"include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"],
|
|
Binary file
|