@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.
- package/.turbo/turbo-build.log +46 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +15 -0
- package/dist/assets/{Arrow.dom--gdrQd-q.js → Arrow.dom-CcoDLP6E.js} +1 -1
- package/dist/assets/{basketViewActivator-CI3y6VYQ.js → basketViewActivator-FtbS__bG.js} +1 -1
- package/dist/assets/{browser-vWDubxDI.js → browser-CXd3z0DO.js} +1 -1
- package/dist/assets/ifc-lite-TI3u_Zyw.js +7 -0
- package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
- package/dist/assets/index-Ba4eoTe7.css +1 -0
- package/dist/assets/{index-BImINgzG.js → index-D99fzcwI.js} +28962 -26947
- package/dist/assets/{index-RXIK18da.js → index-DqNiuQep.js} +4 -4
- package/dist/assets/{native-bridge-4rLidc3f.js → native-bridge-DjDj2M6p.js} +1 -1
- package/dist/assets/{wasm-bridge-BkfXfw8O.js → wasm-bridge-CDTF4ZQc.js} +1 -1
- package/dist/assets/workerHelpers-G7llXNMi.js +36 -0
- package/dist/index.html +2 -2
- package/package.json +15 -14
- package/src/components/viewer/BCFPanel.tsx +12 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +315 -154
- package/src/components/viewer/CommandPalette.tsx +0 -6
- package/src/components/viewer/DataConnector.tsx +489 -284
- package/src/components/viewer/ExportDialog.tsx +66 -6
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +44 -1
- package/src/components/viewer/MainToolbar.tsx +1 -5
- package/src/components/viewer/Viewport.tsx +42 -56
- package/src/components/viewer/ViewportContainer.tsx +3 -0
- package/src/components/viewer/ViewportOverlays.tsx +12 -10
- package/src/components/viewer/bcf/BCFOverlay.tsx +254 -0
- package/src/components/viewer/lists/ListPanel.tsx +0 -21
- package/src/components/viewer/lists/ListResultsTable.tsx +93 -5
- package/src/components/viewer/measureHandlers.ts +558 -0
- package/src/components/viewer/mouseHandlerTypes.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +86 -0
- package/src/components/viewer/useAnimationLoop.ts +116 -44
- package/src/components/viewer/useGeometryStreaming.ts +155 -367
- package/src/components/viewer/useKeyboardControls.ts +30 -46
- package/src/components/viewer/useMouseControls.ts +169 -695
- package/src/components/viewer/useRenderUpdates.ts +9 -59
- package/src/components/viewer/useTouchControls.ts +55 -40
- package/src/hooks/bcfIdLookup.ts +70 -0
- package/src/hooks/useBCF.ts +12 -31
- package/src/hooks/useIfcCache.ts +2 -20
- package/src/hooks/useIfcFederation.ts +5 -11
- package/src/hooks/useIfcLoader.ts +47 -56
- package/src/hooks/useIfcServer.ts +9 -1
- package/src/hooks/useKeyboardShortcuts.ts +0 -10
- package/src/hooks/useLatestRef.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +2 -2
- package/src/sdk/adapters/model-adapter.ts +1 -0
- package/src/sdk/local-backend.ts +2 -0
- package/src/store/basketVisibleSet.ts +12 -0
- package/src/store/slices/bcfSlice.ts +9 -0
- package/src/utils/loadingUtils.ts +46 -0
- package/src/utils/serverDataModel.ts +4 -3
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- 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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|