@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,254 @@
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
+ * BCFOverlay — renders BCF topic markers as 3D-positioned overlays in the viewport.
7
+ *
8
+ * Connects:
9
+ * - Zustand store (BCF topics, active topic)
10
+ * - Renderer (camera projection, entity bounds)
11
+ * - BCFOverlayRenderer (pure DOM marker rendering)
12
+ * - BCF panel (click marker → open topic, bidirectional sync)
13
+ *
14
+ * KEY DESIGN: Bounds lookup queries the renderer Scene directly via a
15
+ * mutable ref (not React state). Marker computation is triggered by an
16
+ * `overlayReady` counter that bumps once the renderer is available AND
17
+ * when loading completes (ensuring bounding boxes are cached).
18
+ * The camera's current distance is passed as `targetDistance` so fallback
19
+ * markers land at the orbit center — not at hardcoded 10 units.
20
+ */
21
+
22
+ import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
23
+ import { useViewerStore } from '@/store';
24
+ import { getGlobalRenderer } from '@/hooks/useBCF';
25
+ import { globalIdToExpressId as globalIdToExpressIdLookup } from '@/hooks/bcfIdLookup';
26
+ import {
27
+ computeMarkerPositions,
28
+ BCFOverlayRenderer,
29
+ type BCFOverlayProjection,
30
+ type OverlayBBox,
31
+ type OverlayPoint3D,
32
+ type EntityBoundsLookup,
33
+ } from '@ifc-lite/bcf';
34
+ import type { Renderer } from '@ifc-lite/renderer';
35
+
36
+ // ============================================================================
37
+ // WebGPU projection adapter
38
+ // ============================================================================
39
+
40
+ function createWebGPUProjection(
41
+ renderer: Renderer,
42
+ canvas: HTMLCanvasElement,
43
+ ): BCFOverlayProjection {
44
+ let prevPosX = NaN;
45
+ let prevPosY = NaN;
46
+ let prevPosZ = NaN;
47
+ let prevTgtX = NaN;
48
+ let prevTgtY = NaN;
49
+ let prevTgtZ = NaN;
50
+ let prevWidth = 0;
51
+ let prevHeight = 0;
52
+
53
+ const listeners = new Set<() => void>();
54
+ let rafId: number | null = null;
55
+ let listenerCount = 0;
56
+
57
+ function poll() {
58
+ rafId = requestAnimationFrame(poll);
59
+ const cam = renderer.getCamera();
60
+ const pos = cam.getPosition();
61
+ const tgt = cam.getTarget();
62
+ const w = canvas.clientWidth;
63
+ const h = canvas.clientHeight;
64
+
65
+ if (
66
+ pos.x !== prevPosX || pos.y !== prevPosY || pos.z !== prevPosZ ||
67
+ tgt.x !== prevTgtX || tgt.y !== prevTgtY || tgt.z !== prevTgtZ ||
68
+ w !== prevWidth || h !== prevHeight
69
+ ) {
70
+ prevPosX = pos.x; prevPosY = pos.y; prevPosZ = pos.z;
71
+ prevTgtX = tgt.x; prevTgtY = tgt.y; prevTgtZ = tgt.z;
72
+ prevWidth = w; prevHeight = h;
73
+ for (const cb of listeners) cb();
74
+ }
75
+ }
76
+
77
+ return {
78
+ projectToScreen(worldPos: OverlayPoint3D) {
79
+ return renderer.getCamera().projectToScreen(
80
+ worldPos,
81
+ canvas.clientWidth,
82
+ canvas.clientHeight,
83
+ );
84
+ },
85
+
86
+ getEntityBounds(expressId: number): OverlayBBox | null {
87
+ return renderer.getScene().getEntityBoundingBox(expressId);
88
+ },
89
+
90
+ getCanvasSize() {
91
+ return { width: canvas.clientWidth, height: canvas.clientHeight };
92
+ },
93
+
94
+ getCameraPosition(): OverlayPoint3D {
95
+ return renderer.getCamera().getPosition();
96
+ },
97
+
98
+ onCameraChange(callback: () => void) {
99
+ listeners.add(callback);
100
+ listenerCount++;
101
+ if (listenerCount === 1) rafId = requestAnimationFrame(poll);
102
+ return () => {
103
+ listeners.delete(callback);
104
+ listenerCount--;
105
+ if (listenerCount === 0 && rafId !== null) {
106
+ cancelAnimationFrame(rafId);
107
+ rafId = null;
108
+ }
109
+ };
110
+ },
111
+ };
112
+ }
113
+
114
+ // ============================================================================
115
+ // React Component
116
+ // ============================================================================
117
+
118
+ export function BCFOverlay() {
119
+ const containerRef = useRef<HTMLDivElement>(null);
120
+ const overlayRef = useRef<BCFOverlayRenderer | null>(null);
121
+ const rendererRef = useRef<Renderer | null>(null);
122
+
123
+ // Bumped when overlay/renderer is ready or geometry finishes loading,
124
+ // triggering marker recomputation with real bounding boxes.
125
+ const [overlayReady, setOverlayReady] = useState(0);
126
+
127
+ // Store selectors
128
+ const bcfProject = useViewerStore((s) => s.bcfProject);
129
+ const activeTopicId = useViewerStore((s) => s.activeTopicId);
130
+ const setActiveTopic = useViewerStore((s) => s.setActiveTopic);
131
+ const setBcfPanelVisible = useViewerStore((s) => s.setBcfPanelVisible);
132
+ const models = useViewerStore((s) => s.models);
133
+ const loading = useViewerStore((s) => s.loading);
134
+ const ifcDataStore = useViewerStore((s) => s.ifcDataStore);
135
+
136
+ // GlobalId → expressId lookup (delegates to shared utility)
137
+ const globalIdToExpressId = useCallback(
138
+ (globalIdString: string) =>
139
+ globalIdToExpressIdLookup(globalIdString, models, ifcDataStore),
140
+ [models, ifcDataStore],
141
+ );
142
+
143
+ // Bounds lookup — queries the renderer Scene directly
144
+ const boundsLookup: EntityBoundsLookup = useCallback(
145
+ (ifcGuid: string): OverlayBBox | null => {
146
+ const r = rendererRef.current;
147
+ if (!r) return null;
148
+ const result = globalIdToExpressId(ifcGuid);
149
+ if (!result) return null;
150
+ return r.getScene().getEntityBoundingBox(result.expressId);
151
+ },
152
+ [globalIdToExpressId],
153
+ );
154
+
155
+ // Get current camera distance (for proper fallback marker placement)
156
+ const getCameraDistance = useCallback((): number => {
157
+ const r = rendererRef.current;
158
+ if (!r) return 50; // safe default
159
+ return r.getCamera().getDistance();
160
+ }, []);
161
+
162
+ // Topics list
163
+ const topics = (() => {
164
+ if (!bcfProject) return [];
165
+ return Array.from(bcfProject.topics.values());
166
+ })();
167
+
168
+ // Compute markers — recomputes when topics, bounds, loading, or readiness changes
169
+ const markers = useMemo(
170
+ () => computeMarkerPositions(topics, boundsLookup, {
171
+ targetDistance: getCameraDistance(),
172
+ }),
173
+ // eslint-disable-next-line react-hooks/exhaustive-deps
174
+ [topics, boundsLookup, overlayReady, loading],
175
+ );
176
+
177
+ // Initialize overlay renderer
178
+ useEffect(() => {
179
+ const container = containerRef.current;
180
+ if (!container) return;
181
+
182
+ const renderer = getGlobalRenderer();
183
+ if (!renderer) return;
184
+
185
+ const canvas = container.closest('[data-viewport]')?.querySelector('canvas') as HTMLCanvasElement | null;
186
+ if (!canvas) return;
187
+
188
+ rendererRef.current = renderer;
189
+
190
+ const projection = createWebGPUProjection(renderer, canvas);
191
+ const overlay = new BCFOverlayRenderer(container, projection, {
192
+ showConnectors: true,
193
+ showTooltips: true,
194
+ verticalOffset: 36,
195
+ });
196
+ overlayRef.current = overlay;
197
+
198
+ // Trigger marker recomputation now that renderer is available
199
+ setOverlayReady((n) => n + 1);
200
+
201
+ return () => {
202
+ overlay.dispose();
203
+ overlayRef.current = null;
204
+ rendererRef.current = null;
205
+ };
206
+ }, [models]);
207
+
208
+ // Recompute markers when loading finishes (bounding boxes get cached)
209
+ useEffect(() => {
210
+ if (!loading && rendererRef.current) {
211
+ setOverlayReady((n) => n + 1);
212
+ }
213
+ }, [loading]);
214
+
215
+ // Push markers to overlay renderer
216
+ useEffect(() => {
217
+ overlayRef.current?.setMarkers(markers);
218
+ }, [markers, overlayReady]);
219
+
220
+ // Sync active marker
221
+ useEffect(() => {
222
+ overlayRef.current?.setActiveMarker(activeTopicId);
223
+ }, [activeTopicId, overlayReady]);
224
+
225
+ // Visibility — reproject markers when becoming visible so they don't
226
+ // sit at stale positions until the next camera move.
227
+ useEffect(() => {
228
+ const overlay = overlayRef.current;
229
+ if (!overlay) return;
230
+ const hasTopics = bcfProject !== null && bcfProject.topics.size > 0;
231
+ overlay.setVisible(hasTopics);
232
+ if (hasTopics) overlay.updatePositions();
233
+ }, [bcfProject, overlayReady]);
234
+
235
+ // Click handler — read bcfPanelVisible from store inside callback to
236
+ // avoid re-registering the handler on every panel toggle.
237
+ useEffect(() => {
238
+ const overlay = overlayRef.current;
239
+ if (!overlay) return;
240
+ return overlay.onMarkerClick((topicGuid) => {
241
+ setActiveTopic(topicGuid);
242
+ const panelVisible = useViewerStore.getState().bcfPanelVisible;
243
+ if (!panelVisible) setBcfPanelVisible(true);
244
+ });
245
+ }, [overlayReady, setActiveTopic, setBcfPanelVisible]);
246
+
247
+ return (
248
+ <div
249
+ ref={containerRef}
250
+ className="absolute inset-0 pointer-events-none z-20"
251
+ data-bcf-overlay
252
+ />
253
+ );
254
+ }
@@ -34,7 +34,6 @@ import { useViewerStore } from '@/store';
34
34
  import { useIfc } from '@/hooks/useIfc';
35
35
  import {
36
36
  executeList,
37
- listResultToCSV,
38
37
  LIST_PRESETS,
39
38
  importListDefinition,
40
39
  exportListDefinition,
@@ -172,18 +171,6 @@ export function ListPanel({ onClose }: ListPanelProps) {
172
171
  }
173
172
  }, [editingList]);
174
173
 
175
- const handleExportCSV = useCallback(() => {
176
- if (!listResult) return;
177
- const csv = listResultToCSV(listResult);
178
- const blob = new Blob([csv], { type: 'text/csv' });
179
- const url = URL.createObjectURL(blob);
180
- const a = document.createElement('a');
181
- a.href = url;
182
- a.download = 'list-export.csv';
183
- a.click();
184
- setTimeout(() => URL.revokeObjectURL(url), 1000);
185
- }, [listResult]);
186
-
187
174
  const handleImport = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
188
175
  const file = e.target.files?.[0];
189
176
  if (!file) return;
@@ -228,14 +215,6 @@ export function ListPanel({ onClose }: ListPanelProps) {
228
215
  </TooltipTrigger>
229
216
  <TooltipContent>Edit Configuration</TooltipContent>
230
217
  </Tooltip>
231
- <Tooltip>
232
- <TooltipTrigger asChild>
233
- <Button variant="ghost" size="icon-sm" onClick={handleExportCSV}>
234
- <Download className="h-3.5 w-3.5" />
235
- </Button>
236
- </TooltipTrigger>
237
- <TooltipContent>Export CSV</TooltipContent>
238
- </Tooltip>
239
218
  <Tooltip>
240
219
  <TooltipTrigger asChild>
241
220
  <Button variant="ghost" size="icon-sm" onClick={() => setView('library')}>
@@ -12,10 +12,14 @@
12
12
 
13
13
  import React, { useCallback, useMemo, useRef, useState } from 'react';
14
14
  import { useVirtualizer } from '@tanstack/react-virtual';
15
- import { ArrowUp, ArrowDown, Search, Palette } from 'lucide-react';
15
+ import { ArrowUp, ArrowDown, Search, Palette, Eye, EyeOff, Download } from 'lucide-react';
16
16
  import { Input } from '@/components/ui/input';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
17
19
  import { useViewerStore } from '@/store';
20
+ import { getVisibleBasketEntityRefsFromStore } from '@/store/basketVisibleSet';
18
21
  import type { ListResult, ListRow, CellValue, ColumnDefinition } from '@ifc-lite/lists';
22
+ import { listResultToCSV } from '@ifc-lite/lists';
19
23
  import { cn } from '@/lib/utils';
20
24
  import { columnToAutoColor } from '@/lib/lists/columnToAutoColor';
21
25
  import { AUTO_COLOR_FROM_LIST_ID } from '@/store/slices/lensSlice';
@@ -29,6 +33,7 @@ export function ListResultsTable({ result }: ListResultsTableProps) {
29
33
  const [searchQuery, setSearchQuery] = useState('');
30
34
  const [sortCol, setSortCol] = useState<number | null>(null);
31
35
  const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
36
+ const [filterByVisibility, setFilterByVisibility] = useState(true);
32
37
 
33
38
  const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
34
39
  const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
@@ -37,14 +42,49 @@ export function ListResultsTable({ result }: ListResultsTableProps) {
37
42
  const activeLensId = useViewerStore((s) => s.activeLensId);
38
43
  const [colorByColIdx, setColorByColIdx] = useState<number | null>(null);
39
44
 
45
+ // Subscribe to visibility state so we re-filter when 3D visibility changes
46
+ const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
47
+ const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
48
+ const classFilter = useViewerStore((s) => s.classFilter);
49
+ const lensHiddenIds = useViewerStore((s) => s.lensHiddenIds);
50
+ const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
51
+ const typeVisibility = useViewerStore((s) => s.typeVisibility);
52
+ const hiddenEntitiesByModel = useViewerStore((s) => s.hiddenEntitiesByModel);
53
+ const isolatedEntitiesByModel = useViewerStore((s) => s.isolatedEntitiesByModel);
54
+ const models = useViewerStore((s) => s.models);
55
+ const activeBasketViewId = useViewerStore((s) => s.activeBasketViewId);
56
+ const geometryResult = useViewerStore((s) => s.geometryResult);
57
+
58
+ // Filter rows by 3D visibility
59
+ const visibilityFilteredRows = useMemo(() => {
60
+ if (!filterByVisibility) return result.rows;
61
+
62
+ const visibleRefs = getVisibleBasketEntityRefsFromStore();
63
+ const visibleSet = new Set<string>();
64
+ for (const ref of visibleRefs) {
65
+ visibleSet.add(`${ref.modelId}:${ref.expressId}`);
66
+ }
67
+
68
+ return result.rows.filter(row => {
69
+ // List uses 'default' for single-model, visibility uses 'legacy'
70
+ const modelId = row.modelId === 'default' ? 'legacy' : row.modelId;
71
+ return visibleSet.has(`${modelId}:${row.entityId}`);
72
+ });
73
+ }, [
74
+ result.rows, filterByVisibility,
75
+ hiddenEntities, isolatedEntities, classFilter, lensHiddenIds,
76
+ selectedStoreys, typeVisibility, hiddenEntitiesByModel,
77
+ isolatedEntitiesByModel, models, activeBasketViewId, geometryResult,
78
+ ]);
79
+
40
80
  // Filter rows by search query
41
81
  const filteredRows = useMemo(() => {
42
- if (!searchQuery) return result.rows;
82
+ if (!searchQuery) return visibilityFilteredRows;
43
83
  const q = searchQuery.toLowerCase();
44
- return result.rows.filter(row =>
84
+ return visibilityFilteredRows.filter(row =>
45
85
  row.values.some(v => v !== null && String(v).toLowerCase().includes(q))
46
86
  );
47
- }, [result.rows, searchQuery]);
87
+ }, [visibilityFilteredRows, searchQuery]);
48
88
 
49
89
  // Sort rows
50
90
  const sortedRows = useMemo(() => {
@@ -74,6 +114,23 @@ export function ListResultsTable({ result }: ListResultsTableProps) {
74
114
  setColorByColIdx(colIdx);
75
115
  }, [activateAutoColorFromColumn]);
76
116
 
117
+ const handleExportCSV = useCallback(() => {
118
+ const exportResult: ListResult = {
119
+ columns: result.columns,
120
+ rows: sortedRows,
121
+ totalCount: sortedRows.length,
122
+ executionTime: result.executionTime,
123
+ };
124
+ const csv = listResultToCSV(exportResult);
125
+ const blob = new Blob([csv], { type: 'text/csv' });
126
+ const url = URL.createObjectURL(blob);
127
+ const a = document.createElement('a');
128
+ a.href = url;
129
+ a.download = 'list-export.csv';
130
+ a.click();
131
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
132
+ }, [result.columns, result.executionTime, sortedRows]);
133
+
77
134
  const handleRowClick = useCallback((row: ListRow) => {
78
135
  setSelectedEntity({ modelId: row.modelId, expressId: row.entityId });
79
136
  // For single-model, selectedEntityId is the expressId
@@ -111,8 +168,39 @@ export function ListResultsTable({ result }: ListResultsTableProps) {
111
168
  className="h-7 text-xs border-0 shadow-none focus-visible:ring-0 px-0"
112
169
  />
113
170
  <span className="text-xs text-muted-foreground whitespace-nowrap">
114
- {sortedRows.length}{searchQuery ? ` / ${result.rows.length}` : ''} rows
171
+ {sortedRows.length}{(searchQuery || filterByVisibility) ? ` / ${result.rows.length}` : ''} rows
115
172
  </span>
173
+ <Tooltip>
174
+ <TooltipTrigger asChild>
175
+ <Button
176
+ variant="ghost"
177
+ size="icon-sm"
178
+ className={cn(
179
+ 'h-6 w-6 shrink-0',
180
+ filterByVisibility && 'text-primary',
181
+ )}
182
+ onClick={() => setFilterByVisibility(prev => !prev)}
183
+ >
184
+ {filterByVisibility ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
185
+ </Button>
186
+ </TooltipTrigger>
187
+ <TooltipContent>
188
+ {filterByVisibility ? 'Showing visible objects only' : 'Showing all objects'}
189
+ </TooltipContent>
190
+ </Tooltip>
191
+ <Tooltip>
192
+ <TooltipTrigger asChild>
193
+ <Button
194
+ variant="ghost"
195
+ size="icon-sm"
196
+ className="h-6 w-6 shrink-0"
197
+ onClick={handleExportCSV}
198
+ >
199
+ <Download className="h-3.5 w-3.5" />
200
+ </Button>
201
+ </TooltipTrigger>
202
+ <TooltipContent>Export CSV</TooltipContent>
203
+ </Tooltip>
116
204
  </div>
117
205
 
118
206
  {/* Table */}