@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,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
|
|
82
|
+
if (!searchQuery) return visibilityFilteredRows;
|
|
43
83
|
const q = searchQuery.toLowerCase();
|
|
44
|
-
return
|
|
84
|
+
return visibilityFilteredRows.filter(row =>
|
|
45
85
|
row.values.some(v => v !== null && String(v).toLowerCase().includes(q))
|
|
46
86
|
);
|
|
47
|
-
}, [
|
|
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 */}
|