@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
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Render updates hook for the 3D viewport
|
|
7
|
-
* Handles visibility/selection/section/hover state re-render effects
|
|
7
|
+
* Handles visibility/selection/section/hover state re-render effects.
|
|
8
|
+
*
|
|
9
|
+
* These effects update refs and request a render — the animation loop
|
|
10
|
+
* picks up the new state on the next frame.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import { useEffect, type MutableRefObject } from 'react';
|
|
@@ -56,7 +59,6 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
56
59
|
isInitialized,
|
|
57
60
|
theme,
|
|
58
61
|
clearColorRef,
|
|
59
|
-
visualEnhancementRef,
|
|
60
62
|
hiddenEntities,
|
|
61
63
|
isolatedEntities,
|
|
62
64
|
selectedEntityId,
|
|
@@ -66,14 +68,8 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
66
68
|
sectionPlane,
|
|
67
69
|
sectionRange,
|
|
68
70
|
coordinateInfo,
|
|
69
|
-
hiddenEntitiesRef,
|
|
70
|
-
isolatedEntitiesRef,
|
|
71
|
-
selectedEntityIdRef,
|
|
72
|
-
selectedModelIndexRef,
|
|
73
|
-
selectedEntityIdsRef,
|
|
74
71
|
sectionPlaneRef,
|
|
75
72
|
sectionRangeRef,
|
|
76
|
-
activeToolRef,
|
|
77
73
|
drawing2D,
|
|
78
74
|
show3DOverlay,
|
|
79
75
|
showHiddenLines,
|
|
@@ -81,20 +77,8 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
81
77
|
|
|
82
78
|
// Theme-aware clear color update
|
|
83
79
|
useEffect(() => {
|
|
84
|
-
// Update clear color when theme changes
|
|
85
80
|
clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark');
|
|
86
|
-
|
|
87
|
-
const renderer = rendererRef.current;
|
|
88
|
-
if (renderer && isInitialized) {
|
|
89
|
-
renderer.render({
|
|
90
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
91
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
92
|
-
selectedId: selectedEntityIdRef.current,
|
|
93
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
94
|
-
clearColor: clearColorRef.current,
|
|
95
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
81
|
+
rendererRef.current?.requestRender();
|
|
98
82
|
}, [theme, isInitialized]);
|
|
99
83
|
|
|
100
84
|
// 2D section overlay: upload drawing data to renderer when available
|
|
@@ -102,16 +86,13 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
102
86
|
const renderer = rendererRef.current;
|
|
103
87
|
if (!renderer || !isInitialized) return;
|
|
104
88
|
|
|
105
|
-
// Only show overlay when section tool is active, we have a drawing, AND 3D overlay is enabled
|
|
106
89
|
if (activeTool === 'section' && drawing2D && drawing2D.cutPolygons.length > 0 && show3DOverlay) {
|
|
107
|
-
// Convert Drawing2D format to renderer format
|
|
108
90
|
const polygons: CutPolygon2D[] = drawing2D.cutPolygons.map((cp) => ({
|
|
109
91
|
polygon: cp.polygon,
|
|
110
92
|
ifcType: cp.ifcType,
|
|
111
|
-
expressId: cp.entityId,
|
|
93
|
+
expressId: cp.entityId,
|
|
112
94
|
}));
|
|
113
95
|
|
|
114
|
-
// Include linework from the generated drawing on the section plane overlay.
|
|
115
96
|
const lines: DrawingLine2D[] = drawing2D.lines
|
|
116
97
|
.filter((line) => showHiddenLines || line.visibility !== 'hidden')
|
|
117
98
|
.map((line) => ({
|
|
@@ -119,36 +100,19 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
119
100
|
category: line.category,
|
|
120
101
|
}));
|
|
121
102
|
|
|
122
|
-
// Upload to renderer - will be drawn on the section plane
|
|
123
|
-
// Pass sectionRange to match exactly what render() uses for section plane position
|
|
124
103
|
renderer.uploadSection2DOverlay(
|
|
125
104
|
polygons,
|
|
126
105
|
lines,
|
|
127
106
|
sectionPlane.axis,
|
|
128
107
|
sectionPlane.position,
|
|
129
|
-
sectionRangeRef.current ?? undefined,
|
|
108
|
+
sectionRangeRef.current ?? undefined,
|
|
130
109
|
sectionPlane.flipped
|
|
131
110
|
);
|
|
132
111
|
} else {
|
|
133
|
-
// Clear overlay when not in section mode, no drawing, or overlay disabled
|
|
134
112
|
renderer.clearSection2DOverlay();
|
|
135
113
|
}
|
|
136
114
|
|
|
137
|
-
|
|
138
|
-
renderer.render({
|
|
139
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
140
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
141
|
-
selectedId: selectedEntityIdRef.current,
|
|
142
|
-
selectedIds: selectedEntityIdsRef.current,
|
|
143
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
144
|
-
clearColor: clearColorRef.current,
|
|
145
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
146
|
-
sectionPlane: activeTool === 'section' ? {
|
|
147
|
-
...sectionPlane,
|
|
148
|
-
min: sectionRangeRef.current?.min,
|
|
149
|
-
max: sectionRangeRef.current?.max,
|
|
150
|
-
} : undefined,
|
|
151
|
-
});
|
|
115
|
+
renderer.requestRender();
|
|
152
116
|
}, [drawing2D, activeTool, sectionPlane, isInitialized, coordinateInfo, show3DOverlay, showHiddenLines]);
|
|
153
117
|
|
|
154
118
|
// Re-render when visibility, selection, or section plane changes
|
|
@@ -156,21 +120,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
156
120
|
const renderer = rendererRef.current;
|
|
157
121
|
if (!renderer || !isInitialized) return;
|
|
158
122
|
|
|
159
|
-
renderer.
|
|
160
|
-
hiddenIds: hiddenEntities,
|
|
161
|
-
isolatedIds: isolatedEntities,
|
|
162
|
-
selectedId: selectedEntityId,
|
|
163
|
-
selectedIds: selectedEntityIds,
|
|
164
|
-
selectedModelIndex,
|
|
165
|
-
clearColor: clearColorRef.current,
|
|
166
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
167
|
-
sectionPlane: activeTool === 'section' ? {
|
|
168
|
-
...sectionPlane,
|
|
169
|
-
min: sectionRange?.min,
|
|
170
|
-
max: sectionRange?.max,
|
|
171
|
-
} : undefined,
|
|
172
|
-
buildingRotation: coordinateInfo?.buildingRotation,
|
|
173
|
-
});
|
|
123
|
+
renderer.requestRender();
|
|
174
124
|
}, [hiddenEntities, isolatedEntities, selectedEntityId, selectedEntityIds, selectedModelIndex, isInitialized, sectionPlane, activeTool, sectionRange, coordinateInfo?.buildingRotation]);
|
|
175
125
|
}
|
|
176
126
|
|
|
@@ -37,6 +37,7 @@ export interface UseTouchControlsParams {
|
|
|
37
37
|
sectionPlaneRef: MutableRefObject<SectionPlane>;
|
|
38
38
|
sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
|
|
39
39
|
geometryRef: MutableRefObject<MeshData[] | null>;
|
|
40
|
+
isInteractingRef: MutableRefObject<boolean>;
|
|
40
41
|
handlePickForSelection: (pickResult: PickResult | null) => void;
|
|
41
42
|
getPickOptions: () => { isStreaming: boolean; hiddenIds: Set<number>; isolatedIds: Set<number> | null };
|
|
42
43
|
}
|
|
@@ -56,6 +57,7 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
56
57
|
sectionPlaneRef,
|
|
57
58
|
sectionRangeRef,
|
|
58
59
|
geometryRef,
|
|
60
|
+
isInteractingRef,
|
|
59
61
|
handlePickForSelection,
|
|
60
62
|
getPickOptions,
|
|
61
63
|
} = params;
|
|
@@ -90,22 +92,38 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
90
92
|
};
|
|
91
93
|
touchState.didMove = false;
|
|
92
94
|
|
|
93
|
-
// Set orbit pivot to
|
|
95
|
+
// Set orbit pivot to the 3D point under the finger.
|
|
96
|
+
// On miss, place pivot at current distance along the finger ray.
|
|
94
97
|
const rect = canvas.getBoundingClientRect();
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const tx = touchState.touches[0].clientX - rect.left;
|
|
99
|
+
const ty = touchState.touches[0].clientY - rect.top;
|
|
100
|
+
const hit = renderer.raycastScene(tx, ty, {
|
|
101
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
102
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
103
|
+
});
|
|
104
|
+
if (hit?.intersection) {
|
|
105
|
+
camera.setOrbitCenter(hit.intersection.point);
|
|
106
|
+
} else if (selectedEntityIdRef.current) {
|
|
107
|
+
const center = getEntityCenter(geometryRef.current, selectedEntityIdRef.current);
|
|
102
108
|
if (center) {
|
|
103
|
-
camera.
|
|
109
|
+
camera.setOrbitCenter(center);
|
|
104
110
|
} else {
|
|
105
|
-
camera.
|
|
111
|
+
camera.setOrbitCenter(null);
|
|
106
112
|
}
|
|
107
113
|
} else {
|
|
108
|
-
camera.
|
|
114
|
+
const ray = camera.unprojectToRay(tx, ty, canvas.width, canvas.height);
|
|
115
|
+
const target = camera.getTarget();
|
|
116
|
+
const toTarget = {
|
|
117
|
+
x: target.x - ray.origin.x,
|
|
118
|
+
y: target.y - ray.origin.y,
|
|
119
|
+
z: target.z - ray.origin.z,
|
|
120
|
+
};
|
|
121
|
+
const d = Math.max(1, toTarget.x * ray.direction.x + toTarget.y * ray.direction.y + toTarget.z * ray.direction.z);
|
|
122
|
+
camera.setOrbitCenter({
|
|
123
|
+
x: ray.origin.x + ray.direction.x * d,
|
|
124
|
+
y: ray.origin.y + ray.direction.y * d,
|
|
125
|
+
z: ray.origin.z + ray.direction.z * d,
|
|
126
|
+
});
|
|
109
127
|
}
|
|
110
128
|
} else if (touchState.touches.length === 1) {
|
|
111
129
|
// Single touch after multi-touch - just update center for orbit
|
|
@@ -144,19 +162,8 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
144
162
|
x: touchState.touches[0].clientX,
|
|
145
163
|
y: touchState.touches[0].clientY,
|
|
146
164
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
150
|
-
selectedId: selectedEntityIdRef.current,
|
|
151
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
152
|
-
clearColor: clearColorRef.current,
|
|
153
|
-
isInteracting: true,
|
|
154
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
155
|
-
...sectionPlaneRef.current,
|
|
156
|
-
min: sectionRangeRef.current?.min,
|
|
157
|
-
max: sectionRangeRef.current?.max,
|
|
158
|
-
} : undefined,
|
|
159
|
-
});
|
|
165
|
+
isInteractingRef.current = true;
|
|
166
|
+
renderer.requestRender();
|
|
160
167
|
} else if (touchState.touches.length === 2) {
|
|
161
168
|
const dx1 = touchState.touches[1].clientX - touchState.touches[0].clientX;
|
|
162
169
|
const dy1 = touchState.touches[1].clientY - touchState.touches[0].clientY;
|
|
@@ -174,19 +181,8 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
174
181
|
|
|
175
182
|
touchState.lastDistance = distance;
|
|
176
183
|
touchState.lastCenter = { x: centerX, y: centerY };
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
180
|
-
selectedId: selectedEntityIdRef.current,
|
|
181
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
182
|
-
clearColor: clearColorRef.current,
|
|
183
|
-
isInteracting: true,
|
|
184
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
185
|
-
...sectionPlaneRef.current,
|
|
186
|
-
min: sectionRangeRef.current?.min,
|
|
187
|
-
max: sectionRangeRef.current?.max,
|
|
188
|
-
} : undefined,
|
|
189
|
-
});
|
|
184
|
+
isInteractingRef.current = true;
|
|
185
|
+
renderer.requestRender();
|
|
190
186
|
}
|
|
191
187
|
};
|
|
192
188
|
|
|
@@ -196,9 +192,16 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
196
192
|
const wasMultiTouch = touchState.multiTouch;
|
|
197
193
|
touchState.touches = Array.from(e.touches);
|
|
198
194
|
|
|
195
|
+
// Only clear interaction when all fingers are lifted (gesture truly ended).
|
|
196
|
+
// Clearing earlier would briefly drop interaction mode during 2-finger → 1-finger
|
|
197
|
+
// transitions, triggering an expensive full-quality render mid-gesture.
|
|
198
|
+
if (touchState.touches.length === 0 && isInteractingRef.current) {
|
|
199
|
+
isInteractingRef.current = false;
|
|
200
|
+
renderer.requestRender();
|
|
201
|
+
}
|
|
202
|
+
|
|
199
203
|
if (touchState.touches.length === 0) {
|
|
200
204
|
camera.stopInertia();
|
|
201
|
-
camera.setOrbitPivot(null);
|
|
202
205
|
|
|
203
206
|
// Tap-to-select: detect quick tap without significant movement
|
|
204
207
|
const tapDuration = Date.now() - touchState.tapStartTime;
|
|
@@ -208,13 +211,12 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
208
211
|
// - Was a single-finger touch (not after multi-touch gesture)
|
|
209
212
|
// - Tap was quick (< 300ms)
|
|
210
213
|
// - Didn't move significantly
|
|
211
|
-
// - Tool supports selection (not
|
|
214
|
+
// - Tool supports selection (not pan/walk/measure)
|
|
212
215
|
if (
|
|
213
216
|
previousTouchCount === 1 &&
|
|
214
217
|
!wasMultiTouch &&
|
|
215
218
|
tapDuration < 300 &&
|
|
216
219
|
!touchState.didMove &&
|
|
217
|
-
tool !== 'orbit' &&
|
|
218
220
|
tool !== 'pan' &&
|
|
219
221
|
tool !== 'walk' &&
|
|
220
222
|
tool !== 'measure'
|
|
@@ -232,14 +234,27 @@ export function useTouchControls(params: UseTouchControlsParams): void {
|
|
|
232
234
|
}
|
|
233
235
|
};
|
|
234
236
|
|
|
237
|
+
// Also reset interaction on touchcancel — mobile browsers can cancel
|
|
238
|
+
// gestures (system gestures, tab switch, lost focus) without touchend.
|
|
239
|
+
const handleTouchCancel = () => {
|
|
240
|
+
if (isInteractingRef.current) {
|
|
241
|
+
isInteractingRef.current = false;
|
|
242
|
+
renderer.requestRender();
|
|
243
|
+
}
|
|
244
|
+
touchState.touches = [];
|
|
245
|
+
touchState.multiTouch = false;
|
|
246
|
+
};
|
|
247
|
+
|
|
235
248
|
canvas.addEventListener('touchstart', handleTouchStart);
|
|
236
249
|
canvas.addEventListener('touchmove', handleTouchMove);
|
|
237
250
|
canvas.addEventListener('touchend', handleTouchEnd);
|
|
251
|
+
canvas.addEventListener('touchcancel', handleTouchCancel);
|
|
238
252
|
|
|
239
253
|
return () => {
|
|
240
254
|
canvas.removeEventListener('touchstart', handleTouchStart);
|
|
241
255
|
canvas.removeEventListener('touchmove', handleTouchMove);
|
|
242
256
|
canvas.removeEventListener('touchend', handleTouchEnd);
|
|
257
|
+
canvas.removeEventListener('touchcancel', handleTouchCancel);
|
|
243
258
|
};
|
|
244
259
|
}, [isInitialized]);
|
|
245
260
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
* Shared BCF ID lookup utilities.
|
|
7
|
+
*
|
|
8
|
+
* Provides conversion between IFC GlobalId strings and expressIds,
|
|
9
|
+
* accounting for multi-model federation offsets and single-model fallback.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FederatedModel, IfcDataStore } from '@/store/types';
|
|
13
|
+
|
|
14
|
+
export interface IdLookupResult {
|
|
15
|
+
expressId: number;
|
|
16
|
+
modelId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert IFC GlobalId string to expressId (with model offset for federation).
|
|
21
|
+
* Searches federated models first, then falls back to the legacy single-model store.
|
|
22
|
+
*/
|
|
23
|
+
export function globalIdToExpressId(
|
|
24
|
+
globalIdString: string,
|
|
25
|
+
models: Map<string, FederatedModel>,
|
|
26
|
+
ifcDataStore: IfcDataStore | null | undefined,
|
|
27
|
+
): IdLookupResult | null {
|
|
28
|
+
// Multi-model path
|
|
29
|
+
for (const [modelId, model] of models.entries()) {
|
|
30
|
+
const localExpressId = model.ifcDataStore?.entities?.getExpressIdByGlobalId(globalIdString);
|
|
31
|
+
if (localExpressId !== undefined && localExpressId > 0) {
|
|
32
|
+
const offset = model.idOffset ?? 0;
|
|
33
|
+
return { expressId: localExpressId + offset, modelId };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Single-model fallback
|
|
37
|
+
if (models.size === 0 && ifcDataStore?.entities) {
|
|
38
|
+
const localExpressId = ifcDataStore.entities.getExpressIdByGlobalId(globalIdString);
|
|
39
|
+
if (localExpressId !== undefined && localExpressId > 0) {
|
|
40
|
+
return { expressId: localExpressId, modelId: 'legacy' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert expressId to IFC GlobalId string (reversing federation offset).
|
|
48
|
+
* Searches federated models first, then falls back to the legacy single-model store.
|
|
49
|
+
*/
|
|
50
|
+
export function expressIdToGlobalId(
|
|
51
|
+
expressId: number,
|
|
52
|
+
models: Map<string, FederatedModel>,
|
|
53
|
+
ifcDataStore: IfcDataStore | null | undefined,
|
|
54
|
+
): string | null {
|
|
55
|
+
// Multi-model path: search federated models
|
|
56
|
+
for (const model of models.values()) {
|
|
57
|
+
const offset = model.idOffset ?? 0;
|
|
58
|
+
const localExpressId = expressId - offset;
|
|
59
|
+
if (localExpressId > 0 && localExpressId <= (model.maxExpressId ?? Infinity)) {
|
|
60
|
+
const globalIdString = model.ifcDataStore?.entities?.getGlobalId(localExpressId);
|
|
61
|
+
if (globalIdString) return globalIdString;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Single-model fallback: use legacy ifcDataStore directly
|
|
65
|
+
if (models.size === 0 && ifcDataStore?.entities) {
|
|
66
|
+
const globalIdString = ifcDataStore.entities.getGlobalId(expressId);
|
|
67
|
+
if (globalIdString) return globalIdString;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
package/src/hooks/useBCF.ts
CHANGED
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
type ViewerBounds,
|
|
23
23
|
} from '@ifc-lite/bcf';
|
|
24
24
|
import type { Renderer } from '@ifc-lite/renderer';
|
|
25
|
+
import {
|
|
26
|
+
globalIdToExpressId as globalIdToExpressIdLookup,
|
|
27
|
+
expressIdToGlobalId as expressIdToGlobalIdLookup,
|
|
28
|
+
} from './bcfIdLookup';
|
|
25
29
|
|
|
26
30
|
// ============================================================================
|
|
27
31
|
// Types
|
|
@@ -122,6 +126,8 @@ export function useBCF(options: UseBCFOptions = {}): UseBCFResult {
|
|
|
122
126
|
|
|
123
127
|
// Get coordinate info for bounds
|
|
124
128
|
const models = useViewerStore((s) => s.models);
|
|
129
|
+
// Legacy single-model data store (used when models Map is empty)
|
|
130
|
+
const ifcDataStore = useViewerStore((s) => s.ifcDataStore);
|
|
125
131
|
|
|
126
132
|
/**
|
|
127
133
|
* Get the canvas element (local ref or global)
|
|
@@ -225,22 +231,9 @@ export function useBCF(options: UseBCFOptions = {}): UseBCFResult {
|
|
|
225
231
|
* Handles multi-model federation by finding the correct model and subtracting offset
|
|
226
232
|
*/
|
|
227
233
|
const expressIdToGlobalId = useCallback(
|
|
228
|
-
(expressId: number): string | null =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const localExpressId = expressId - offset;
|
|
232
|
-
|
|
233
|
-
// Check if this expressId belongs to this model's range
|
|
234
|
-
if (localExpressId > 0 && localExpressId <= (model.maxExpressId ?? Infinity)) {
|
|
235
|
-
const globalIdString = model.ifcDataStore?.entities?.getGlobalId(localExpressId);
|
|
236
|
-
if (globalIdString) {
|
|
237
|
-
return globalIdString;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
},
|
|
243
|
-
[models]
|
|
234
|
+
(expressId: number): string | null =>
|
|
235
|
+
expressIdToGlobalIdLookup(expressId, models, ifcDataStore),
|
|
236
|
+
[models, ifcDataStore]
|
|
244
237
|
);
|
|
245
238
|
|
|
246
239
|
/**
|
|
@@ -248,21 +241,9 @@ export function useBCF(options: UseBCFOptions = {}): UseBCFResult {
|
|
|
248
241
|
* Returns { expressId, modelId } or null if not found
|
|
249
242
|
*/
|
|
250
243
|
const globalIdToExpressId = useCallback(
|
|
251
|
-
(globalIdString: string): { expressId: number; modelId: string } | null =>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (localExpressId !== undefined && localExpressId > 0) {
|
|
255
|
-
// Add model offset for federation
|
|
256
|
-
const offset = model.idOffset ?? 0;
|
|
257
|
-
return {
|
|
258
|
-
expressId: localExpressId + offset,
|
|
259
|
-
modelId,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return null;
|
|
264
|
-
},
|
|
265
|
-
[models]
|
|
244
|
+
(globalIdString: string): { expressId: number; modelId: string } | null =>
|
|
245
|
+
globalIdToExpressIdLookup(globalIdString, models, ifcDataStore),
|
|
246
|
+
[models, ifcDataStore]
|
|
266
247
|
);
|
|
267
248
|
|
|
268
249
|
/**
|
package/src/hooks/useIfcCache.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type GeometryData,
|
|
18
18
|
} from '@ifc-lite/cache';
|
|
19
19
|
import { SpatialHierarchyBuilder, StepTokenizer, buildCompactEntityIndex, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
20
|
-
import {
|
|
20
|
+
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
21
21
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
22
22
|
|
|
23
23
|
import { useShallow } from 'zustand/react/shallow';
|
|
@@ -191,25 +191,7 @@ export function useIfcCache() {
|
|
|
191
191
|
// Set data store
|
|
192
192
|
setIfcDataStore(dataStore);
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
if (meshes.length > 0) {
|
|
196
|
-
const buildIndex = () => {
|
|
197
|
-
try {
|
|
198
|
-
const spatialIndex = buildSpatialIndex(meshes);
|
|
199
|
-
dataStore.spatialIndex = spatialIndex;
|
|
200
|
-
setIfcDataStore({ ...dataStore });
|
|
201
|
-
} catch (err) {
|
|
202
|
-
console.warn('[useIfcCache] Failed to build spatial index:', err);
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
if ('requestIdleCallback' in window) {
|
|
207
|
-
(window as any).requestIdleCallback(buildIndex, { timeout: 2000 });
|
|
208
|
-
} else {
|
|
209
|
-
// Fallback for browsers without requestIdleCallback
|
|
210
|
-
setTimeout(buildIndex, 100);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
194
|
+
buildSpatialIndexGuarded(meshes, dataStore, setIfcDataStore);
|
|
213
195
|
} else {
|
|
214
196
|
setIfcDataStore(dataStore);
|
|
215
197
|
}
|
|
@@ -16,7 +16,7 @@ import { useViewerStore, type FederatedModel, type SchemaVersion } from '../stor
|
|
|
16
16
|
import { IfcParser, detectFormat, parseIfcx, parseFederatedIfcx, type IfcDataStore, type FederatedIfcxParseResult } from '@ifc-lite/parser';
|
|
17
17
|
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
18
18
|
import { IfcQuery } from '@ifc-lite/query';
|
|
19
|
-
import {
|
|
19
|
+
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
20
20
|
import { loadGLBToMeshData } from '@ifc-lite/cache';
|
|
21
21
|
|
|
22
22
|
import { getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
@@ -333,16 +333,6 @@ export function useIfcFederation() {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// Build spatial index
|
|
337
|
-
if (allMeshes.length > 0) {
|
|
338
|
-
try {
|
|
339
|
-
const spatialIndex = buildSpatialIndex(allMeshes);
|
|
340
|
-
parsedDataStore.spatialIndex = spatialIndex;
|
|
341
|
-
} catch (err) {
|
|
342
|
-
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
336
|
parsedGeometry = {
|
|
347
337
|
meshes: allMeshes,
|
|
348
338
|
totalVertices: allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0),
|
|
@@ -463,6 +453,10 @@ export function useIfcFederation() {
|
|
|
463
453
|
}
|
|
464
454
|
}
|
|
465
455
|
|
|
456
|
+
// Build spatial index AFTER ID offset + RTC alignment so it stores
|
|
457
|
+
// correct globalIds and final world-space positions.
|
|
458
|
+
buildSpatialIndexGuarded(parsedGeometry.meshes, parsedDataStore, setIfcDataStore);
|
|
459
|
+
|
|
466
460
|
// Create the federated model with offset info
|
|
467
461
|
const federatedModel: FederatedModel = {
|
|
468
462
|
id: modelId,
|