@ifc-lite/viewer 1.16.0 → 1.17.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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +46 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/CHANGELOG.md +15 -0
  4. package/dist/assets/{Arrow.dom--gdrQd-q.js → Arrow.dom-CcoDLP6E.js} +1 -1
  5. package/dist/assets/{basketViewActivator-CI3y6VYQ.js → basketViewActivator-FtbS__bG.js} +1 -1
  6. package/dist/assets/{browser-vWDubxDI.js → browser-CXd3z0DO.js} +1 -1
  7. package/dist/assets/ifc-lite-TI3u_Zyw.js +7 -0
  8. package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
  9. package/dist/assets/index-Ba4eoTe7.css +1 -0
  10. package/dist/assets/{index-BImINgzG.js → index-D99fzcwI.js} +28962 -26947
  11. package/dist/assets/{index-RXIK18da.js → index-DqNiuQep.js} +4 -4
  12. package/dist/assets/{native-bridge-4rLidc3f.js → native-bridge-DjDj2M6p.js} +1 -1
  13. package/dist/assets/{wasm-bridge-BkfXfw8O.js → wasm-bridge-CDTF4ZQc.js} +1 -1
  14. package/dist/assets/workerHelpers-G7llXNMi.js +36 -0
  15. package/dist/index.html +2 -2
  16. package/package.json +15 -14
  17. package/src/components/viewer/BCFPanel.tsx +12 -0
  18. package/src/components/viewer/BulkPropertyEditor.tsx +315 -154
  19. package/src/components/viewer/CommandPalette.tsx +0 -6
  20. package/src/components/viewer/DataConnector.tsx +489 -284
  21. package/src/components/viewer/ExportDialog.tsx +66 -6
  22. package/src/components/viewer/KeyboardShortcutsDialog.tsx +44 -1
  23. package/src/components/viewer/MainToolbar.tsx +1 -5
  24. package/src/components/viewer/Viewport.tsx +42 -56
  25. package/src/components/viewer/ViewportContainer.tsx +3 -0
  26. package/src/components/viewer/ViewportOverlays.tsx +12 -10
  27. package/src/components/viewer/bcf/BCFOverlay.tsx +254 -0
  28. package/src/components/viewer/lists/ListPanel.tsx +0 -21
  29. package/src/components/viewer/lists/ListResultsTable.tsx +93 -5
  30. package/src/components/viewer/measureHandlers.ts +558 -0
  31. package/src/components/viewer/mouseHandlerTypes.ts +108 -0
  32. package/src/components/viewer/selectionHandlers.ts +86 -0
  33. package/src/components/viewer/useAnimationLoop.ts +116 -44
  34. package/src/components/viewer/useGeometryStreaming.ts +155 -367
  35. package/src/components/viewer/useKeyboardControls.ts +30 -46
  36. package/src/components/viewer/useMouseControls.ts +169 -695
  37. package/src/components/viewer/useRenderUpdates.ts +9 -59
  38. package/src/components/viewer/useTouchControls.ts +55 -40
  39. package/src/hooks/bcfIdLookup.ts +70 -0
  40. package/src/hooks/useBCF.ts +12 -31
  41. package/src/hooks/useIfcCache.ts +2 -20
  42. package/src/hooks/useIfcFederation.ts +5 -11
  43. package/src/hooks/useIfcLoader.ts +47 -56
  44. package/src/hooks/useIfcServer.ts +9 -1
  45. package/src/hooks/useKeyboardShortcuts.ts +0 -10
  46. package/src/hooks/useLatestRef.ts +24 -0
  47. package/src/sdk/adapters/export-adapter.ts +2 -2
  48. package/src/sdk/adapters/model-adapter.ts +1 -0
  49. package/src/sdk/local-backend.ts +2 -0
  50. package/src/store/basketVisibleSet.ts +12 -0
  51. package/src/store/slices/bcfSlice.ts +9 -0
  52. package/src/utils/loadingUtils.ts +46 -0
  53. package/src/utils/serverDataModel.ts +4 -3
  54. package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
  55. package/dist/assets/index-ax1X2WPd.css +0 -1
@@ -0,0 +1,86 @@
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
+ * Selection handler functions extracted from useMouseControls.
7
+ * Handles click/double-click selection and context menu interactions.
8
+ * Pure functions that operate on a MouseHandlerContext — no React dependency.
9
+ */
10
+
11
+ import type { MouseHandlerContext } from './mouseHandlerTypes.js';
12
+
13
+ /**
14
+ * Handle click event for selection (single click and double click).
15
+ * Manages click timing for double-click detection and Ctrl/Cmd multi-select.
16
+ */
17
+ export async function handleSelectionClick(ctx: MouseHandlerContext, e: MouseEvent): Promise<void> {
18
+ const { canvas, renderer, mouseState } = ctx;
19
+ const rect = canvas.getBoundingClientRect();
20
+ const x = e.clientX - rect.left;
21
+ const y = e.clientY - rect.top;
22
+ const tool = ctx.activeToolRef.current;
23
+
24
+ // Skip selection if user was dragging (orbiting/panning)
25
+ if (mouseState.didDrag) {
26
+ return;
27
+ }
28
+
29
+ // Skip selection for pan/walk tools - they don't select
30
+ if (tool === 'pan' || tool === 'walk') {
31
+ return;
32
+ }
33
+
34
+ // Measure tool now uses drag interaction (see mousedown/mousemove/mouseup)
35
+ if (tool === 'measure') {
36
+ return; // Skip click handling for measure tool
37
+ }
38
+
39
+ const now = Date.now();
40
+ const timeSinceLastClick = now - ctx.lastClickTimeRef.current;
41
+ const clickPos = { x, y };
42
+
43
+ if (ctx.lastClickPosRef.current &&
44
+ timeSinceLastClick < 300 &&
45
+ Math.abs(clickPos.x - ctx.lastClickPosRef.current.x) < 5 &&
46
+ Math.abs(clickPos.y - ctx.lastClickPosRef.current.y) < 5) {
47
+ // Double-click - isolate element
48
+ // Uses visibility filtering so only visible elements can be selected
49
+ const pickResult = await renderer.pick(x, y, ctx.getPickOptions());
50
+ if (pickResult) {
51
+ ctx.handlePickForSelection(pickResult);
52
+ }
53
+ ctx.lastClickTimeRef.current = 0;
54
+ ctx.lastClickPosRef.current = null;
55
+ } else {
56
+ // Single click - uses visibility filtering so only visible elements can be selected
57
+ const pickResult = await renderer.pick(x, y, ctx.getPickOptions());
58
+
59
+ // Multi-selection with Ctrl/Cmd
60
+ if (e.ctrlKey || e.metaKey) {
61
+ if (pickResult) {
62
+ ctx.toggleSelection(pickResult.expressId);
63
+ }
64
+ } else {
65
+ ctx.handlePickForSelection(pickResult);
66
+ }
67
+
68
+ ctx.lastClickTimeRef.current = now;
69
+ ctx.lastClickPosRef.current = clickPos;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Handle context menu event (right-click).
75
+ * Picks the entity under the cursor and opens the context menu.
76
+ */
77
+ export async function handleContextMenu(ctx: MouseHandlerContext, e: MouseEvent): Promise<void> {
78
+ e.preventDefault();
79
+ const { canvas, renderer } = ctx;
80
+ const rect = canvas.getBoundingClientRect();
81
+ const x = e.clientX - rect.left;
82
+ const y = e.clientY - rect.top;
83
+ // Uses visibility filtering so hidden elements don't appear in context menu
84
+ const pickResult = await renderer.pick(x, y, ctx.getPickOptions());
85
+ ctx.openContextMenu(pickResult?.expressId ?? null, e.clientX, e.clientY);
86
+ }
@@ -3,12 +3,22 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
5
  /**
6
- * Animation loop hook for the 3D viewport
7
- * Handles requestAnimationFrame loop, camera update, ViewCube sync
6
+ * THE render loop for the 3D viewport.
7
+ *
8
+ * This is the single place where renderer.render() is called during normal
9
+ * operation. Everything else (mouse, touch, keyboard, streaming, visibility
10
+ * changes, theme, lens) calls renderer.requestRender() to set a dirty flag.
11
+ *
12
+ * Each frame:
13
+ * 1. Drain the scene's mesh queue (streaming uploads with time budget).
14
+ * 2. Update camera (animation / inertia).
15
+ * 3. If dirty OR animating → render with current state from refs.
16
+ * 4. Sync ViewCube, scale bar, measurements.
8
17
  */
9
18
 
10
19
  import { useEffect, type MutableRefObject, type RefObject } from 'react';
11
20
  import type { Renderer, VisualEnhancementOptions } from '@ifc-lite/renderer';
21
+ import type { CoordinateInfo } from '@ifc-lite/geometry';
12
22
  import type { SectionPlane } from '@/store';
13
23
 
14
24
  export interface UseAnimationLoopParams {
@@ -27,6 +37,9 @@ export interface UseAnimationLoopParams {
27
37
  visualEnhancementRef: MutableRefObject<VisualEnhancementOptions>;
28
38
  sectionPlaneRef: MutableRefObject<SectionPlane>;
29
39
  sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
40
+ selectedEntityIdsRef: MutableRefObject<Set<number> | undefined>;
41
+ coordinateInfoRef: MutableRefObject<CoordinateInfo | undefined>;
42
+ isInteractingRef: MutableRefObject<boolean>;
30
43
  lastCameraStateRef: MutableRefObject<{
31
44
  position: { x: number; y: number; z: number };
32
45
  rotation: { azimuth: number; elevation: number };
@@ -57,6 +70,9 @@ export function useAnimationLoop(params: UseAnimationLoopParams): void {
57
70
  visualEnhancementRef,
58
71
  sectionPlaneRef,
59
72
  sectionRangeRef,
73
+ selectedEntityIdsRef,
74
+ coordinateInfoRef,
75
+ isInteractingRef,
60
76
  lastCameraStateRef,
61
77
  updateCameraRotationRealtime,
62
78
  calculateScale,
@@ -70,89 +86,145 @@ export function useAnimationLoop(params: UseAnimationLoopParams): void {
70
86
  if (!renderer || !canvas || !isInitialized) return;
71
87
 
72
88
  const camera = renderer.getCamera();
89
+ const scene = renderer.getScene();
73
90
  let aborted = false;
74
91
 
75
- // Animation loop - update ViewCube in real-time
76
92
  let lastRotationUpdate = 0;
77
93
  let lastScaleUpdate = 0;
94
+ let lastRenderTime = 0;
95
+
96
+ // Adaptive render throttle: large models get fewer FPS during continuous
97
+ // rendering (interaction + inertia) to prevent the main thread from being
98
+ // overwhelmed. Model "size" is measured by total triangle count across all
99
+ // batched geometry — individual mesh count is near 0 for batched models.
100
+ let continuousThrottleMs = 0; // 0 = no throttle (small models)
101
+
102
+ function updateThrottle() {
103
+ let totalIndices = 0;
104
+ for (const batch of scene.getBatchedMeshes()) {
105
+ totalIndices += batch.indexCount;
106
+ }
107
+ // Also account for individual meshes
108
+ totalIndices += scene.getMeshes().reduce((s, m) => s + (m.indexCount ?? 0), 0);
109
+ const triangles = totalIndices / 3;
110
+ if (triangles > 5_000_000) {
111
+ continuousThrottleMs = 33; // ~30 fps — huge models (>5M triangles)
112
+ } else if (triangles > 1_000_000) {
113
+ continuousThrottleMs = 25; // ~40 fps — large models (>1M triangles)
114
+ } else {
115
+ continuousThrottleMs = 0;
116
+ }
117
+ }
118
+ updateThrottle();
119
+
78
120
  const animate = (currentTime: number) => {
79
121
  if (aborted) return;
80
122
 
81
123
  const deltaTime = currentTime - lastFrameTimeRef.current;
82
124
  lastFrameTimeRef.current = currentTime;
83
125
 
126
+ // 1. Drain mesh queue (streaming GPU uploads)
127
+ let queueFlushed = false;
128
+ if (scene.hasQueuedMeshes()) {
129
+ const device = renderer.getGPUDevice();
130
+ const pipeline = renderer.getPipeline();
131
+ if (device && pipeline) {
132
+ queueFlushed = scene.flushPending(device, pipeline);
133
+ if (queueFlushed) {
134
+ renderer.clearCaches();
135
+ updateThrottle();
136
+ }
137
+ }
138
+ }
139
+
140
+ // 2. Camera update (animation / inertia)
84
141
  const isAnimating = camera.update(deltaTime);
85
- if (isAnimating) {
142
+
143
+ // 3. Render if anything changed
144
+ // Peek first — only consume the flag when we actually commit to rendering.
145
+ // This prevents a throttled frame from eating the dirty flag.
146
+ const renderRequested = renderer.peekRenderRequest();
147
+
148
+ // Throttle render rate during continuous rendering (interaction + inertia)
149
+ // for large models. Without this, 200K+ mesh models at 60fps overwhelm
150
+ // the main thread and freeze the tab. Inertia alone can run 60+ frames
151
+ // after mouseup, each requiring a full GPU render pass.
152
+ const isContinuousRender = isInteractingRef.current || isAnimating;
153
+ const throttled = isContinuousRender &&
154
+ continuousThrottleMs > 0 &&
155
+ (currentTime - lastRenderTime) < continuousThrottleMs;
156
+
157
+ if ((isAnimating || renderRequested || queueFlushed) && !throttled) {
158
+ renderer.consumeRenderRequest();
86
159
  renderer.render({
87
160
  hiddenIds: hiddenEntitiesRef.current,
88
161
  isolatedIds: isolatedEntitiesRef.current,
89
162
  selectedId: selectedEntityIdRef.current,
163
+ selectedIds: selectedEntityIdsRef.current,
90
164
  selectedModelIndex: selectedModelIndexRef.current,
91
165
  clearColor: clearColorRef.current,
92
166
  visualEnhancement: visualEnhancementRef.current,
167
+ isInteracting: isInteractingRef.current || isAnimating,
168
+ buildingRotation: coordinateInfoRef.current?.buildingRotation,
93
169
  sectionPlane: activeToolRef.current === 'section' ? {
94
170
  ...sectionPlaneRef.current,
95
171
  min: sectionRangeRef.current?.min,
96
172
  max: sectionRangeRef.current?.max,
97
173
  } : undefined,
98
174
  });
99
- // Update ViewCube during camera animation (e.g., preset view transitions)
175
+ lastRenderTime = currentTime;
176
+ }
177
+
178
+ // 4. Sync UI widgets
179
+ if (isAnimating || renderRequested || queueFlushed) {
100
180
  updateCameraRotationRealtime(camera.getRotation());
101
181
  calculateScale();
102
182
  } else if (!mouseIsDraggingRef.current && currentTime - lastRotationUpdate > 500) {
103
- // Update camera rotation for ViewCube when not dragging (throttled to every 500ms when idle)
104
183
  updateCameraRotationRealtime(camera.getRotation());
105
184
  lastRotationUpdate = currentTime;
106
185
  }
107
186
 
108
- // Update scale bar (throttled to every 500ms - scale rarely needs frequent updates)
109
187
  if (currentTime - lastScaleUpdate > 500) {
110
188
  calculateScale();
111
189
  lastScaleUpdate = currentTime;
112
190
  }
113
191
 
114
- // Update measurement screen coordinates only when:
115
- // 1. Measure tool is active (not in other modes)
116
- // 2. Measurements exist
117
- // 3. Camera actually changed
118
- // This prevents unnecessary store updates and re-renders when not measuring
119
- if (activeToolRef.current === 'measure') {
120
- if (hasPendingMeasurements()) {
121
- const cameraPos = camera.getPosition();
122
- const cameraRot = camera.getRotation();
123
- const cameraDist = camera.getDistance();
124
- const currentCameraState = {
125
- position: cameraPos,
126
- rotation: cameraRot,
127
- distance: cameraDist,
128
- canvasWidth: canvas.width,
129
- canvasHeight: canvas.height,
130
- };
131
-
132
- // Check if camera state changed
133
- const lastState = lastCameraStateRef.current;
134
- const cameraChanged =
135
- !lastState ||
136
- lastState.position.x !== currentCameraState.position.x ||
137
- lastState.position.y !== currentCameraState.position.y ||
138
- lastState.position.z !== currentCameraState.position.z ||
139
- lastState.rotation.azimuth !== currentCameraState.rotation.azimuth ||
140
- lastState.rotation.elevation !== currentCameraState.rotation.elevation ||
141
- lastState.distance !== currentCameraState.distance ||
142
- lastState.canvasWidth !== currentCameraState.canvasWidth ||
143
- lastState.canvasHeight !== currentCameraState.canvasHeight;
144
-
145
- if (cameraChanged) {
146
- lastCameraStateRef.current = currentCameraState;
147
- updateMeasurementScreenCoords((worldPos) => {
148
- return camera.projectToScreen(worldPos, canvas.width, canvas.height);
149
- });
150
- }
192
+ // 5. Measurement screen coords
193
+ if (activeToolRef.current === 'measure' && hasPendingMeasurements()) {
194
+ const cameraPos = camera.getPosition();
195
+ const cameraRot = camera.getRotation();
196
+ const cameraDist = camera.getDistance();
197
+ const currentCameraState = {
198
+ position: cameraPos,
199
+ rotation: cameraRot,
200
+ distance: cameraDist,
201
+ canvasWidth: canvas.width,
202
+ canvasHeight: canvas.height,
203
+ };
204
+
205
+ const lastState = lastCameraStateRef.current;
206
+ const cameraChanged =
207
+ !lastState ||
208
+ lastState.position.x !== currentCameraState.position.x ||
209
+ lastState.position.y !== currentCameraState.position.y ||
210
+ lastState.position.z !== currentCameraState.position.z ||
211
+ lastState.rotation.azimuth !== currentCameraState.rotation.azimuth ||
212
+ lastState.rotation.elevation !== currentCameraState.rotation.elevation ||
213
+ lastState.distance !== currentCameraState.distance ||
214
+ lastState.canvasWidth !== currentCameraState.canvasWidth ||
215
+ lastState.canvasHeight !== currentCameraState.canvasHeight;
216
+
217
+ if (cameraChanged) {
218
+ lastCameraStateRef.current = currentCameraState;
219
+ updateMeasurementScreenCoords((worldPos) => {
220
+ return camera.projectToScreen(worldPos, canvas.width, canvas.height);
221
+ });
151
222
  }
152
223
  }
153
224
 
154
225
  animationFrameRef.current = requestAnimationFrame(animate);
155
226
  };
227
+
156
228
  lastFrameTimeRef.current = performance.now();
157
229
  animationFrameRef.current = requestAnimationFrame(animate);
158
230