@ifc-lite/viewer 1.1.6 → 1.5.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/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -0,0 +1,504 @@
|
|
|
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
|
+
* BCF (BIM Collaboration Format) hook
|
|
7
|
+
*
|
|
8
|
+
* Provides functions to create and apply BCF viewpoints, including:
|
|
9
|
+
* - Capturing snapshots from the WebGPU canvas
|
|
10
|
+
* - Converting between viewer camera state and BCF viewpoint format
|
|
11
|
+
* - Applying viewpoints to the viewer (camera, selection, visibility)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useRef } from 'react';
|
|
15
|
+
import { useViewerStore } from '@/store';
|
|
16
|
+
import type { BCFViewpoint } from '@ifc-lite/bcf';
|
|
17
|
+
import {
|
|
18
|
+
createViewpoint,
|
|
19
|
+
extractViewpointState,
|
|
20
|
+
type ViewerCameraState,
|
|
21
|
+
type ViewerSectionPlane,
|
|
22
|
+
type ViewerBounds,
|
|
23
|
+
} from '@ifc-lite/bcf';
|
|
24
|
+
import type { Renderer } from '@ifc-lite/renderer';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Types
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
interface UseBCFOptions {
|
|
31
|
+
/** Ref to the WebGPU canvas for snapshot capture */
|
|
32
|
+
canvasRef?: React.RefObject<HTMLCanvasElement>;
|
|
33
|
+
/** Ref to the renderer for camera access */
|
|
34
|
+
rendererRef?: React.RefObject<Renderer | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CreateViewpointOptions {
|
|
38
|
+
/** Include a snapshot image */
|
|
39
|
+
includeSnapshot?: boolean;
|
|
40
|
+
/** Include selected entities */
|
|
41
|
+
includeSelection?: boolean;
|
|
42
|
+
/** Include hidden entities */
|
|
43
|
+
includeHidden?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface UseBCFResult {
|
|
47
|
+
/** Create a viewpoint from current viewer state */
|
|
48
|
+
createViewpointFromState: (options?: CreateViewpointOptions) => Promise<BCFViewpoint | null>;
|
|
49
|
+
/** Apply a viewpoint to the viewer */
|
|
50
|
+
applyViewpoint: (viewpoint: BCFViewpoint, animate?: boolean) => void;
|
|
51
|
+
/** Capture a snapshot from the canvas */
|
|
52
|
+
captureSnapshot: () => Promise<string | null>;
|
|
53
|
+
/** Set the canvas ref for snapshot capture */
|
|
54
|
+
setCanvasRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
|
|
55
|
+
/** Set the renderer ref for camera access */
|
|
56
|
+
setRendererRef: (ref: React.RefObject<Renderer | null>) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Canvas Reference Store (module-level for cross-component access)
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
let globalCanvasRef: React.RefObject<HTMLCanvasElement> | null = null;
|
|
64
|
+
let globalRendererRef: React.RefObject<Renderer | null> | null = null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set the global canvas reference (called by ViewportContainer)
|
|
68
|
+
*/
|
|
69
|
+
export function setGlobalCanvasRef(ref: React.RefObject<HTMLCanvasElement>): void {
|
|
70
|
+
globalCanvasRef = ref;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the global renderer reference (called by ViewportContainer)
|
|
75
|
+
*/
|
|
76
|
+
export function setGlobalRendererRef(ref: React.RefObject<Renderer | null>): void {
|
|
77
|
+
globalRendererRef = ref;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear the global references (called on unmount to prevent memory leaks)
|
|
82
|
+
*/
|
|
83
|
+
export function clearGlobalRefs(): void {
|
|
84
|
+
globalCanvasRef = null;
|
|
85
|
+
globalRendererRef = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Hook
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
export function useBCF(options: UseBCFOptions = {}): UseBCFResult {
|
|
93
|
+
const localCanvasRef = useRef<React.RefObject<HTMLCanvasElement> | null>(
|
|
94
|
+
options.canvasRef ?? null
|
|
95
|
+
);
|
|
96
|
+
const localRendererRef = useRef<React.RefObject<Renderer | null> | null>(
|
|
97
|
+
options.rendererRef ?? null
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Store selectors
|
|
101
|
+
const sectionPlane = useViewerStore((s) => s.sectionPlane);
|
|
102
|
+
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
103
|
+
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
104
|
+
const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
|
|
105
|
+
const selectedEntityIds = useViewerStore((s) => s.selectedEntityIds);
|
|
106
|
+
const setSectionPlaneAxis = useViewerStore((s) => s.setSectionPlaneAxis);
|
|
107
|
+
const setSectionPlanePosition = useViewerStore((s) => s.setSectionPlanePosition);
|
|
108
|
+
const toggleSectionPlane = useViewerStore((s) => s.toggleSectionPlane);
|
|
109
|
+
const flipSectionPlane = useViewerStore((s) => s.flipSectionPlane);
|
|
110
|
+
|
|
111
|
+
// Selection and visibility actions
|
|
112
|
+
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
113
|
+
const setHiddenEntities = useViewerStore((s) => s.setHiddenEntities);
|
|
114
|
+
const setIsolatedEntities = useViewerStore((s) => s.setIsolatedEntities);
|
|
115
|
+
|
|
116
|
+
// Get coordinate info for bounds
|
|
117
|
+
const models = useViewerStore((s) => s.models);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the canvas element (local ref or global)
|
|
121
|
+
*/
|
|
122
|
+
const getCanvas = useCallback((): HTMLCanvasElement | null => {
|
|
123
|
+
return localCanvasRef.current?.current ?? globalCanvasRef?.current ?? null;
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the renderer instance (local ref or global)
|
|
128
|
+
*/
|
|
129
|
+
const getRenderer = useCallback((): Renderer | null => {
|
|
130
|
+
return localRendererRef.current?.current ?? globalRendererRef?.current ?? null;
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set the canvas ref for snapshot capture
|
|
135
|
+
*/
|
|
136
|
+
const setCanvasRef = useCallback((ref: React.RefObject<HTMLCanvasElement>) => {
|
|
137
|
+
localCanvasRef.current = ref;
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set the renderer ref for camera access
|
|
142
|
+
*/
|
|
143
|
+
const setRendererRef = useCallback((ref: React.RefObject<Renderer | null>) => {
|
|
144
|
+
localRendererRef.current = ref;
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Capture a snapshot from the WebGPU canvas
|
|
149
|
+
* Captures exactly what the user sees - no re-rendering
|
|
150
|
+
*/
|
|
151
|
+
const captureSnapshot = useCallback(async (): Promise<string | null> => {
|
|
152
|
+
const canvas = getCanvas();
|
|
153
|
+
const renderer = getRenderer();
|
|
154
|
+
if (!canvas) {
|
|
155
|
+
console.warn('[useBCF] No canvas available for snapshot capture');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Wait for any pending GPU work to complete before capturing
|
|
161
|
+
// This ensures we capture the fully rendered frame
|
|
162
|
+
if (renderer) {
|
|
163
|
+
const device = renderer.getGPUDevice();
|
|
164
|
+
if (device) {
|
|
165
|
+
await device.queue.onSubmittedWorkDone();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Capture exactly what's displayed on the canvas
|
|
170
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
171
|
+
return dataUrl;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[useBCF] Failed to capture snapshot:', error);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}, [getCanvas, getRenderer]);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get current camera state from renderer
|
|
180
|
+
*/
|
|
181
|
+
const getCameraState = useCallback((): ViewerCameraState | null => {
|
|
182
|
+
const renderer = getRenderer();
|
|
183
|
+
if (!renderer) {
|
|
184
|
+
console.warn('[useBCF] No renderer available for camera state');
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const camera = renderer.getCamera();
|
|
189
|
+
const position = camera.getPosition();
|
|
190
|
+
const target = camera.getTarget();
|
|
191
|
+
const up = camera.getUp();
|
|
192
|
+
const fov = camera.getFOV();
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
position,
|
|
196
|
+
target,
|
|
197
|
+
up, // Use actual camera up vector
|
|
198
|
+
fov,
|
|
199
|
+
isOrthographic: false,
|
|
200
|
+
};
|
|
201
|
+
}, [getRenderer]);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get model bounds from loaded models
|
|
205
|
+
*/
|
|
206
|
+
const getBounds = useCallback((): ViewerBounds | null => {
|
|
207
|
+
// Get bounds from first loaded model's geometry result
|
|
208
|
+
for (const model of models.values()) {
|
|
209
|
+
if (model.geometryResult?.coordinateInfo?.shiftedBounds) {
|
|
210
|
+
return model.geometryResult.coordinateInfo.shiftedBounds;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}, [models]);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Convert expressId (with model offset) to IFC GlobalId string
|
|
218
|
+
* Handles multi-model federation by finding the correct model and subtracting offset
|
|
219
|
+
*/
|
|
220
|
+
const expressIdToGlobalId = useCallback(
|
|
221
|
+
(expressId: number): string | null => {
|
|
222
|
+
for (const model of models.values()) {
|
|
223
|
+
const offset = model.idOffset ?? 0;
|
|
224
|
+
const localExpressId = expressId - offset;
|
|
225
|
+
|
|
226
|
+
// Check if this expressId belongs to this model's range
|
|
227
|
+
if (localExpressId > 0 && localExpressId <= (model.maxExpressId ?? Infinity)) {
|
|
228
|
+
const globalIdString = model.ifcDataStore?.entities?.getGlobalId(localExpressId);
|
|
229
|
+
if (globalIdString) {
|
|
230
|
+
return globalIdString;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
},
|
|
236
|
+
[models]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert IFC GlobalId string to expressId (with model offset for federation)
|
|
241
|
+
* Returns { expressId, modelId } or null if not found
|
|
242
|
+
*/
|
|
243
|
+
const globalIdToExpressId = useCallback(
|
|
244
|
+
(globalIdString: string): { expressId: number; modelId: string } | null => {
|
|
245
|
+
for (const [modelId, model] of models.entries()) {
|
|
246
|
+
const localExpressId = model.ifcDataStore?.entities?.getExpressIdByGlobalId(globalIdString);
|
|
247
|
+
if (localExpressId !== undefined && localExpressId > 0) {
|
|
248
|
+
// Add model offset for federation
|
|
249
|
+
const offset = model.idOffset ?? 0;
|
|
250
|
+
return {
|
|
251
|
+
expressId: localExpressId + offset,
|
|
252
|
+
modelId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
},
|
|
258
|
+
[models]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a viewpoint from current viewer state
|
|
263
|
+
*/
|
|
264
|
+
const createViewpointFromState = useCallback(
|
|
265
|
+
async (opts: CreateViewpointOptions = {}): Promise<BCFViewpoint | null> => {
|
|
266
|
+
const {
|
|
267
|
+
includeSnapshot = true,
|
|
268
|
+
includeSelection = true,
|
|
269
|
+
includeHidden = true,
|
|
270
|
+
} = opts;
|
|
271
|
+
|
|
272
|
+
const cameraState = getCameraState();
|
|
273
|
+
if (!cameraState) {
|
|
274
|
+
console.warn('[useBCF] Cannot create viewpoint: no camera state');
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Get snapshot if requested
|
|
279
|
+
let snapshot: string | undefined;
|
|
280
|
+
if (includeSnapshot) {
|
|
281
|
+
const captured = await captureSnapshot();
|
|
282
|
+
if (captured) {
|
|
283
|
+
snapshot = captured;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Convert section plane state
|
|
288
|
+
const viewerSectionPlane: ViewerSectionPlane | undefined = sectionPlane.enabled
|
|
289
|
+
? {
|
|
290
|
+
axis: sectionPlane.axis,
|
|
291
|
+
position: sectionPlane.position,
|
|
292
|
+
enabled: true,
|
|
293
|
+
flipped: sectionPlane.flipped,
|
|
294
|
+
}
|
|
295
|
+
: undefined;
|
|
296
|
+
|
|
297
|
+
// Get bounds for section plane conversion
|
|
298
|
+
const bounds = getBounds() ?? undefined;
|
|
299
|
+
|
|
300
|
+
// Get selected GUIDs - convert expressIds to IFC GlobalId strings
|
|
301
|
+
const selectedGuids: string[] | undefined = includeSelection
|
|
302
|
+
? (() => {
|
|
303
|
+
const guids: string[] = [];
|
|
304
|
+
if (selectedEntityId !== null) {
|
|
305
|
+
const guid = expressIdToGlobalId(selectedEntityId);
|
|
306
|
+
if (guid) guids.push(guid);
|
|
307
|
+
}
|
|
308
|
+
for (const id of selectedEntityIds) {
|
|
309
|
+
if (id !== selectedEntityId) {
|
|
310
|
+
const guid = expressIdToGlobalId(id);
|
|
311
|
+
if (guid) guids.push(guid);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return guids.length > 0 ? guids : undefined;
|
|
315
|
+
})()
|
|
316
|
+
: undefined;
|
|
317
|
+
|
|
318
|
+
// Get visibility GUIDs - either hidden (normal mode) or visible (isolation mode)
|
|
319
|
+
let hiddenGuids: string[] | undefined;
|
|
320
|
+
let visibleGuids: string[] | undefined;
|
|
321
|
+
|
|
322
|
+
if (includeHidden) {
|
|
323
|
+
if (isolatedEntities !== null && isolatedEntities.size > 0) {
|
|
324
|
+
// Isolation mode: capture visible entities (defaultVisibility=false)
|
|
325
|
+
const guids: string[] = [];
|
|
326
|
+
for (const id of isolatedEntities) {
|
|
327
|
+
const guid = expressIdToGlobalId(id);
|
|
328
|
+
if (guid) guids.push(guid);
|
|
329
|
+
}
|
|
330
|
+
visibleGuids = guids.length > 0 ? guids : undefined;
|
|
331
|
+
} else if (hiddenEntities.size > 0) {
|
|
332
|
+
// Normal mode: capture hidden entities (defaultVisibility=true)
|
|
333
|
+
const guids: string[] = [];
|
|
334
|
+
for (const id of hiddenEntities) {
|
|
335
|
+
const guid = expressIdToGlobalId(id);
|
|
336
|
+
if (guid) guids.push(guid);
|
|
337
|
+
}
|
|
338
|
+
hiddenGuids = guids.length > 0 ? guids : undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create viewpoint
|
|
343
|
+
return createViewpoint({
|
|
344
|
+
camera: cameraState,
|
|
345
|
+
sectionPlane: viewerSectionPlane,
|
|
346
|
+
bounds,
|
|
347
|
+
snapshot,
|
|
348
|
+
selectedGuids,
|
|
349
|
+
hiddenGuids,
|
|
350
|
+
visibleGuids,
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
[
|
|
354
|
+
getCameraState,
|
|
355
|
+
captureSnapshot,
|
|
356
|
+
sectionPlane,
|
|
357
|
+
getBounds,
|
|
358
|
+
selectedEntityId,
|
|
359
|
+
selectedEntityIds,
|
|
360
|
+
hiddenEntities,
|
|
361
|
+
isolatedEntities,
|
|
362
|
+
expressIdToGlobalId,
|
|
363
|
+
]
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Apply a viewpoint to the viewer
|
|
368
|
+
*/
|
|
369
|
+
const applyViewpoint = useCallback(
|
|
370
|
+
(viewpoint: BCFViewpoint, animate = true) => {
|
|
371
|
+
const renderer = getRenderer();
|
|
372
|
+
if (!renderer) {
|
|
373
|
+
console.warn('[useBCF] Cannot apply viewpoint: no renderer');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const bounds = getBounds() ?? undefined;
|
|
378
|
+
|
|
379
|
+
// Extract state from viewpoint (once, reused for camera, section plane, and selection)
|
|
380
|
+
const state = extractViewpointState(
|
|
381
|
+
viewpoint,
|
|
382
|
+
bounds,
|
|
383
|
+
renderer.getCamera().getDistance() // Use current distance as reference
|
|
384
|
+
);
|
|
385
|
+
const { camera, sectionPlane: viewpointSectionPlane } = state;
|
|
386
|
+
|
|
387
|
+
// Apply camera
|
|
388
|
+
if (camera) {
|
|
389
|
+
const rendererCamera = renderer.getCamera();
|
|
390
|
+
|
|
391
|
+
if (animate) {
|
|
392
|
+
// Animate to new position
|
|
393
|
+
rendererCamera.animateTo(
|
|
394
|
+
camera.position,
|
|
395
|
+
camera.target,
|
|
396
|
+
300 // 300ms animation
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
// Set immediately
|
|
400
|
+
rendererCamera.setPosition(camera.position.x, camera.position.y, camera.position.z);
|
|
401
|
+
rendererCamera.setTarget(camera.target.x, camera.target.y, camera.target.z);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Apply section plane
|
|
406
|
+
if (viewpointSectionPlane) {
|
|
407
|
+
// Set axis and position
|
|
408
|
+
setSectionPlaneAxis(viewpointSectionPlane.axis);
|
|
409
|
+
setSectionPlanePosition(viewpointSectionPlane.position);
|
|
410
|
+
|
|
411
|
+
// Toggle enabled state if needed
|
|
412
|
+
const currentEnabled = sectionPlane.enabled;
|
|
413
|
+
if (viewpointSectionPlane.enabled !== currentEnabled) {
|
|
414
|
+
toggleSectionPlane();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Toggle flip state if needed
|
|
418
|
+
const currentFlipped = sectionPlane.flipped;
|
|
419
|
+
if (viewpointSectionPlane.flipped !== currentFlipped) {
|
|
420
|
+
flipSectionPlane();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Apply selection from BCF components
|
|
425
|
+
if (state.selectedGuids.length > 0) {
|
|
426
|
+
// Convert GlobalId strings to expressIds
|
|
427
|
+
const selectedExpressIds: number[] = [];
|
|
428
|
+
for (const guid of state.selectedGuids) {
|
|
429
|
+
const result = globalIdToExpressId(guid);
|
|
430
|
+
if (result) {
|
|
431
|
+
selectedExpressIds.push(result.expressId);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (selectedExpressIds.length > 0) {
|
|
436
|
+
// Select the first entity (primary selection)
|
|
437
|
+
// The expressId here already includes the federation offset
|
|
438
|
+
setSelectedEntityId(selectedExpressIds[0]);
|
|
439
|
+
// Note: Multi-selection would require additional store support
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
// Clear selection if viewpoint has no selection
|
|
443
|
+
setSelectedEntityId(null);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Apply visibility from BCF components
|
|
447
|
+
// Either isolation mode (visibleGuids with defaultVisibility=false)
|
|
448
|
+
// or normal mode (hiddenGuids with defaultVisibility=true)
|
|
449
|
+
if (state.visibleGuids.length > 0) {
|
|
450
|
+
// Isolation mode: only specified entities are visible
|
|
451
|
+
const isolatedExpressIds = new Set<number>();
|
|
452
|
+
for (const guid of state.visibleGuids) {
|
|
453
|
+
const result = globalIdToExpressId(guid);
|
|
454
|
+
if (result) {
|
|
455
|
+
isolatedExpressIds.add(result.expressId);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (isolatedExpressIds.size > 0) {
|
|
460
|
+
setIsolatedEntities(isolatedExpressIds);
|
|
461
|
+
} else {
|
|
462
|
+
setIsolatedEntities(null);
|
|
463
|
+
}
|
|
464
|
+
} else if (state.hiddenGuids.length > 0) {
|
|
465
|
+
// Normal mode: specified entities are hidden
|
|
466
|
+
const hiddenExpressIds = new Set<number>();
|
|
467
|
+
for (const guid of state.hiddenGuids) {
|
|
468
|
+
const result = globalIdToExpressId(guid);
|
|
469
|
+
if (result) {
|
|
470
|
+
hiddenExpressIds.add(result.expressId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (hiddenExpressIds.size > 0) {
|
|
475
|
+
setHiddenEntities(hiddenExpressIds);
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
// Clear all visibility state if viewpoint has none
|
|
479
|
+
setHiddenEntities(new Set());
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
[
|
|
483
|
+
getRenderer,
|
|
484
|
+
getBounds,
|
|
485
|
+
sectionPlane,
|
|
486
|
+
setSectionPlaneAxis,
|
|
487
|
+
setSectionPlanePosition,
|
|
488
|
+
toggleSectionPlane,
|
|
489
|
+
flipSectionPlane,
|
|
490
|
+
globalIdToExpressId,
|
|
491
|
+
setSelectedEntityId,
|
|
492
|
+
setHiddenEntities,
|
|
493
|
+
setIsolatedEntities,
|
|
494
|
+
]
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
createViewpointFromState,
|
|
499
|
+
applyViewpoint,
|
|
500
|
+
captureSnapshot,
|
|
501
|
+
setCanvasRef,
|
|
502
|
+
setRendererRef,
|
|
503
|
+
};
|
|
504
|
+
}
|