@ifc-lite/viewer 1.7.0 → 1.9.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/CHANGELOG.md +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import React, { useRef, useCallback, useMemo } from 'react';
|
|
5
|
+
import React, { useRef, useCallback, useEffect, useMemo } from 'react';
|
|
6
6
|
import {
|
|
7
7
|
FolderOpen,
|
|
8
8
|
Download,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
Scissors,
|
|
15
15
|
Eye,
|
|
16
16
|
EyeOff,
|
|
17
|
-
|
|
17
|
+
Equal,
|
|
18
18
|
Crosshair,
|
|
19
19
|
Home,
|
|
20
20
|
Maximize2,
|
|
@@ -34,13 +34,11 @@ import {
|
|
|
34
34
|
SquareX,
|
|
35
35
|
Building2,
|
|
36
36
|
Plus,
|
|
37
|
+
Minus,
|
|
37
38
|
MessageSquare,
|
|
38
39
|
ClipboardCheck,
|
|
39
|
-
Pin,
|
|
40
|
-
PinOff,
|
|
41
40
|
Palette,
|
|
42
41
|
Orbit,
|
|
43
|
-
Trash2,
|
|
44
42
|
} from 'lucide-react';
|
|
45
43
|
import { Button } from '@/components/ui/button';
|
|
46
44
|
import { Separator } from '@/components/ui/separator';
|
|
@@ -57,7 +55,8 @@ import {
|
|
|
57
55
|
DropdownMenuSubContent,
|
|
58
56
|
} from '@/components/ui/dropdown-menu';
|
|
59
57
|
import { Progress } from '@/components/ui/progress';
|
|
60
|
-
import { useViewerStore, isIfcxDataStore } from '@/store';
|
|
58
|
+
import { useViewerStore, isIfcxDataStore, stringToEntityRef } from '@/store';
|
|
59
|
+
import type { EntityRef } from '@/store';
|
|
61
60
|
import { useIfc } from '@/hooks/useIfc';
|
|
62
61
|
import { cn } from '@/lib/utils';
|
|
63
62
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
@@ -67,6 +66,7 @@ import { BulkPropertyEditor } from './BulkPropertyEditor';
|
|
|
67
66
|
import { DataConnector } from './DataConnector';
|
|
68
67
|
import { ExportChangesButton } from './ExportChangesButton';
|
|
69
68
|
import { useFloorplanView } from '@/hooks/useFloorplanView';
|
|
69
|
+
import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
|
|
70
70
|
|
|
71
71
|
type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
|
|
72
72
|
|
|
@@ -148,6 +148,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
148
148
|
const addModelInputRef = useRef<HTMLInputElement>(null);
|
|
149
149
|
const { loadFile, loading, progress, geometryResult, ifcDataStore, models, clearAllModels, loadFilesSequentially, loadFederatedIfcx, addIfcxOverlays, addModel } = useIfc();
|
|
150
150
|
|
|
151
|
+
// Listen for programmatic file-load requests (from command palette recent files)
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const handler = (e: Event) => {
|
|
154
|
+
const file = (e as CustomEvent<File>).detail;
|
|
155
|
+
if (file) loadFile(file);
|
|
156
|
+
};
|
|
157
|
+
window.addEventListener('ifc-lite:load-file', handler);
|
|
158
|
+
return () => window.removeEventListener('ifc-lite:load-file', handler);
|
|
159
|
+
}, [loadFile]);
|
|
160
|
+
|
|
151
161
|
// Floorplan view
|
|
152
162
|
const { availableStoreys, activateFloorplan } = useFloorplanView();
|
|
153
163
|
|
|
@@ -158,8 +168,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
158
168
|
const theme = useViewerStore((state) => state.theme);
|
|
159
169
|
const toggleTheme = useViewerStore((state) => state.toggleTheme);
|
|
160
170
|
const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
|
|
161
|
-
const
|
|
162
|
-
const hideEntity = useViewerStore((state) => state.hideEntity);
|
|
171
|
+
const hideEntities = useViewerStore((state) => state.hideEntities);
|
|
163
172
|
const showAll = useViewerStore((state) => state.showAll);
|
|
164
173
|
const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
|
|
165
174
|
const error = useViewerStore((state) => state.error);
|
|
@@ -171,23 +180,27 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
171
180
|
const resetViewerState = useViewerStore((state) => state.resetViewerState);
|
|
172
181
|
const bcfPanelVisible = useViewerStore((state) => state.bcfPanelVisible);
|
|
173
182
|
const toggleBcfPanel = useViewerStore((state) => state.toggleBcfPanel);
|
|
183
|
+
const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
|
|
174
184
|
const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
|
|
175
185
|
const toggleIdsPanel = useViewerStore((state) => state.toggleIdsPanel);
|
|
186
|
+
const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
|
|
176
187
|
const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
|
|
177
188
|
const toggleListPanel = useViewerStore((state) => state.toggleListPanel);
|
|
178
189
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
179
190
|
const projectionMode = useViewerStore((state) => state.projectionMode);
|
|
180
191
|
const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
|
|
181
|
-
//
|
|
192
|
+
// Basket state (= + − isolation basket)
|
|
182
193
|
const pinboardEntities = useViewerStore((state) => state.pinboardEntities);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
194
|
+
const setBasket = useViewerStore((state) => state.setBasket);
|
|
195
|
+
const addToBasket = useViewerStore((state) => state.addToBasket);
|
|
196
|
+
const removeFromBasket = useViewerStore((state) => state.removeFromBasket);
|
|
197
|
+
const clearBasket = useViewerStore((state) => state.clearBasket);
|
|
198
|
+
// NOTE: selectedEntity and selectedEntitiesSet accessed via getState() in callbacks
|
|
199
|
+
// to avoid re-rendering MainToolbar on every Cmd+Click selection change.
|
|
188
200
|
// Lens state
|
|
189
201
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
190
202
|
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
203
|
+
const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
|
|
191
204
|
|
|
192
205
|
// Check which type geometries exist across ALL loaded models (federation-aware)
|
|
193
206
|
const typeGeometryExists = useMemo(() => {
|
|
@@ -232,6 +245,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
232
245
|
|
|
233
246
|
if (supportedFiles.length === 0) return;
|
|
234
247
|
|
|
248
|
+
// Track recently opened files (metadata + blob cache for instant reload)
|
|
249
|
+
recordRecentFiles(supportedFiles.map(f => ({ name: f.name, size: f.size })));
|
|
250
|
+
cacheFileBlobs(supportedFiles);
|
|
251
|
+
|
|
235
252
|
if (supportedFiles.length === 1) {
|
|
236
253
|
// Single file - use loadFile (simpler single-model path)
|
|
237
254
|
loadFile(supportedFiles[0]);
|
|
@@ -289,25 +306,85 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
289
306
|
e.target.value = '';
|
|
290
307
|
}, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
|
|
291
308
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
/** Get current selection as EntityRef[] — uses getState() to avoid reactive subscriptions */
|
|
310
|
+
const getSelectionRefs = useCallback((): EntityRef[] => {
|
|
311
|
+
const state = useViewerStore.getState();
|
|
312
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
313
|
+
const refs: EntityRef[] = [];
|
|
314
|
+
for (const str of state.selectedEntitiesSet) {
|
|
315
|
+
refs.push(stringToEntityRef(str));
|
|
316
|
+
}
|
|
317
|
+
return refs;
|
|
318
|
+
}
|
|
319
|
+
if (state.selectedEntity) {
|
|
320
|
+
return [state.selectedEntity];
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
const hasSelection = selectedEntityId !== null;
|
|
326
|
+
|
|
327
|
+
// Basket state
|
|
328
|
+
const showPinboard = useViewerStore((state) => state.showPinboard);
|
|
329
|
+
|
|
330
|
+
// Clear multi-select state after basket operations so subsequent − targets a single entity
|
|
331
|
+
const clearMultiSelect = useCallback(() => {
|
|
332
|
+
const state = useViewerStore.getState();
|
|
333
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
334
|
+
useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
|
|
335
|
+
}
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
// Basket operations
|
|
339
|
+
const handleSetBasket = useCallback(() => {
|
|
340
|
+
const state = useViewerStore.getState();
|
|
341
|
+
// If basket already exists and user hasn't explicitly multi-selected,
|
|
342
|
+
// re-apply the basket instead of replacing it with a stale single selection.
|
|
343
|
+
// Only an explicit multi-selection (Ctrl+Click) should replace an existing basket.
|
|
344
|
+
if (state.pinboardEntities.size > 0 && state.selectedEntitiesSet.size === 0) {
|
|
345
|
+
showPinboard();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const refs = getSelectionRefs();
|
|
349
|
+
if (refs.length > 0) {
|
|
350
|
+
setBasket(refs);
|
|
351
|
+
clearMultiSelect();
|
|
295
352
|
}
|
|
296
|
-
}, [
|
|
353
|
+
}, [getSelectionRefs, setBasket, showPinboard, clearMultiSelect]);
|
|
354
|
+
|
|
355
|
+
const handleAddToBasket = useCallback(() => {
|
|
356
|
+
const refs = getSelectionRefs();
|
|
357
|
+
if (refs.length > 0) {
|
|
358
|
+
addToBasket(refs);
|
|
359
|
+
clearMultiSelect();
|
|
360
|
+
}
|
|
361
|
+
}, [getSelectionRefs, addToBasket, clearMultiSelect]);
|
|
362
|
+
|
|
363
|
+
const handleRemoveFromBasket = useCallback(() => {
|
|
364
|
+
const refs = getSelectionRefs();
|
|
365
|
+
if (refs.length > 0) {
|
|
366
|
+
removeFromBasket(refs);
|
|
367
|
+
clearMultiSelect();
|
|
368
|
+
}
|
|
369
|
+
}, [getSelectionRefs, removeFromBasket, clearMultiSelect]);
|
|
297
370
|
|
|
298
371
|
const clearSelection = useViewerStore((state) => state.clearSelection);
|
|
299
372
|
|
|
300
373
|
const handleHide = useCallback(() => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
374
|
+
// Hide ALL selected entities (multi-select or single)
|
|
375
|
+
const state = useViewerStore.getState();
|
|
376
|
+
const ids: number[] = state.selectedEntityIds.size > 0
|
|
377
|
+
? Array.from(state.selectedEntityIds)
|
|
378
|
+
: selectedEntityId !== null ? [selectedEntityId] : [];
|
|
379
|
+
if (ids.length > 0) {
|
|
380
|
+
hideEntities(ids);
|
|
304
381
|
clearSelection();
|
|
305
382
|
}
|
|
306
|
-
}, [selectedEntityId,
|
|
383
|
+
}, [selectedEntityId, hideEntities, clearSelection]);
|
|
307
384
|
|
|
308
385
|
const handleShowAll = useCallback(() => {
|
|
309
|
-
showAll();
|
|
310
|
-
clearStoreySelection(); // Also clear storey filtering
|
|
386
|
+
showAll(); // Clear hiddenEntities + isolatedEntities (basket contents preserved)
|
|
387
|
+
clearStoreySelection(); // Also clear storey filtering
|
|
311
388
|
}, [showAll, clearStoreySelection]);
|
|
312
389
|
|
|
313
390
|
const handleExportGLB = useCallback(() => {
|
|
@@ -413,6 +490,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
413
490
|
<div className="flex items-center gap-1 px-2 h-12 border-b bg-white dark:bg-black border-zinc-200 dark:border-zinc-800 relative z-50">
|
|
414
491
|
{/* ── File Operations ── */}
|
|
415
492
|
<input
|
|
493
|
+
id="file-input-open"
|
|
416
494
|
ref={fileInputRef}
|
|
417
495
|
type="file"
|
|
418
496
|
accept=".ifc,.ifcx,.glb"
|
|
@@ -574,8 +652,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
574
652
|
size="icon-sm"
|
|
575
653
|
onClick={(e) => {
|
|
576
654
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
577
|
-
// If BCF is being shown, also expand the right panel
|
|
578
655
|
if (!bcfPanelVisible) {
|
|
656
|
+
// Close other right-panel content first, then expand
|
|
657
|
+
setIdsPanelVisible(false);
|
|
658
|
+
setLensPanelVisible(false);
|
|
579
659
|
setRightPanelCollapsed(false);
|
|
580
660
|
}
|
|
581
661
|
toggleBcfPanel();
|
|
@@ -596,8 +676,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
596
676
|
size="icon-sm"
|
|
597
677
|
onClick={(e) => {
|
|
598
678
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
599
|
-
// If IDS is being shown, also expand the right panel
|
|
600
679
|
if (!idsPanelVisible) {
|
|
680
|
+
// Close other right-panel content first, then expand
|
|
681
|
+
setBcfPanelVisible(false);
|
|
682
|
+
setLensPanelVisible(false);
|
|
601
683
|
setRightPanelCollapsed(false);
|
|
602
684
|
}
|
|
603
685
|
toggleIdsPanel();
|
|
@@ -618,6 +700,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
618
700
|
size="icon-sm"
|
|
619
701
|
onClick={(e) => {
|
|
620
702
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
703
|
+
// Close script panel (bottom-panel exclusivity)
|
|
704
|
+
useViewerStore.getState().setScriptPanelVisible(false);
|
|
621
705
|
if (!listPanelVisible) {
|
|
622
706
|
setRightPanelCollapsed(false);
|
|
623
707
|
}
|
|
@@ -675,9 +759,37 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
675
759
|
|
|
676
760
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
677
761
|
|
|
678
|
-
{/* ──
|
|
679
|
-
<
|
|
680
|
-
|
|
762
|
+
{/* ── Basket Isolation (= + −) ── */}
|
|
763
|
+
<Tooltip>
|
|
764
|
+
<TooltipTrigger asChild>
|
|
765
|
+
<Button
|
|
766
|
+
variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
|
|
767
|
+
size="icon-sm"
|
|
768
|
+
onClick={(e) => {
|
|
769
|
+
(e.currentTarget as HTMLButtonElement).blur();
|
|
770
|
+
handleSetBasket();
|
|
771
|
+
}}
|
|
772
|
+
disabled={!hasSelection && pinboardEntities.size === 0}
|
|
773
|
+
className={cn(
|
|
774
|
+
pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative',
|
|
775
|
+
)}
|
|
776
|
+
>
|
|
777
|
+
<Equal className="h-4 w-4" />
|
|
778
|
+
{pinboardEntities.size > 0 && (
|
|
779
|
+
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[9px] font-bold rounded-full min-w-[14px] h-[14px] flex items-center justify-center px-0.5 border border-background">
|
|
780
|
+
{pinboardEntities.size}
|
|
781
|
+
</span>
|
|
782
|
+
)}
|
|
783
|
+
</Button>
|
|
784
|
+
</TooltipTrigger>
|
|
785
|
+
<TooltipContent>
|
|
786
|
+
Set Basket — isolate selection <span className="ml-2 text-xs opacity-60">(I)</span>
|
|
787
|
+
</TooltipContent>
|
|
788
|
+
</Tooltip>
|
|
789
|
+
<ActionButton icon={Plus} label="Add to Basket" onClick={handleAddToBasket} shortcut="+" disabled={!hasSelection} />
|
|
790
|
+
<ActionButton icon={Minus} label="Remove from Basket" onClick={handleRemoveFromBasket} shortcut="−" disabled={!hasSelection} />
|
|
791
|
+
|
|
792
|
+
<ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" disabled={!hasSelection} />
|
|
681
793
|
<ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
|
|
682
794
|
<ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
|
|
683
795
|
<ActionButton
|
|
@@ -685,7 +797,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
685
797
|
label="Frame Selection"
|
|
686
798
|
onClick={() => cameraCallbacks.frameSelection?.()}
|
|
687
799
|
shortcut="F"
|
|
688
|
-
disabled={!
|
|
800
|
+
disabled={!hasSelection}
|
|
689
801
|
/>
|
|
690
802
|
|
|
691
803
|
<DropdownMenu>
|
|
@@ -730,60 +842,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
730
842
|
</DropdownMenuContent>
|
|
731
843
|
</DropdownMenu>
|
|
732
844
|
|
|
733
|
-
{/* Pinboard dropdown */}
|
|
734
|
-
<DropdownMenu>
|
|
735
|
-
<Tooltip>
|
|
736
|
-
<TooltipTrigger asChild>
|
|
737
|
-
<DropdownMenuTrigger asChild>
|
|
738
|
-
<Button
|
|
739
|
-
variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
|
|
740
|
-
size="icon-sm"
|
|
741
|
-
className={cn(pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative')}
|
|
742
|
-
>
|
|
743
|
-
<Pin className="h-4 w-4" />
|
|
744
|
-
{pinboardEntities.size > 0 && (
|
|
745
|
-
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[9px] font-bold rounded-full min-w-[14px] h-[14px] flex items-center justify-center px-0.5 border border-background">
|
|
746
|
-
{pinboardEntities.size}
|
|
747
|
-
</span>
|
|
748
|
-
)}
|
|
749
|
-
</Button>
|
|
750
|
-
</DropdownMenuTrigger>
|
|
751
|
-
</TooltipTrigger>
|
|
752
|
-
<TooltipContent>Pinboard ({pinboardEntities.size})</TooltipContent>
|
|
753
|
-
</Tooltip>
|
|
754
|
-
<DropdownMenuContent>
|
|
755
|
-
<DropdownMenuItem
|
|
756
|
-
onClick={() => { if (selectedEntity) addToPinboard([selectedEntity]); }}
|
|
757
|
-
disabled={!selectedEntity}
|
|
758
|
-
>
|
|
759
|
-
<Pin className="h-4 w-4 mr-2" />
|
|
760
|
-
Pin Selection
|
|
761
|
-
</DropdownMenuItem>
|
|
762
|
-
<DropdownMenuItem
|
|
763
|
-
onClick={() => { if (selectedEntity) removeFromPinboard([selectedEntity]); }}
|
|
764
|
-
disabled={!selectedEntity}
|
|
765
|
-
>
|
|
766
|
-
<PinOff className="h-4 w-4 mr-2" />
|
|
767
|
-
Unpin Selection
|
|
768
|
-
</DropdownMenuItem>
|
|
769
|
-
<DropdownMenuSeparator />
|
|
770
|
-
<DropdownMenuItem
|
|
771
|
-
onClick={() => showPinboard()}
|
|
772
|
-
disabled={pinboardEntities.size === 0}
|
|
773
|
-
>
|
|
774
|
-
<Eye className="h-4 w-4 mr-2" />
|
|
775
|
-
Show Pinboard
|
|
776
|
-
</DropdownMenuItem>
|
|
777
|
-
<DropdownMenuItem
|
|
778
|
-
onClick={() => clearPinboard()}
|
|
779
|
-
disabled={pinboardEntities.size === 0}
|
|
780
|
-
>
|
|
781
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
782
|
-
Clear Pinboard
|
|
783
|
-
</DropdownMenuItem>
|
|
784
|
-
</DropdownMenuContent>
|
|
785
|
-
</DropdownMenu>
|
|
786
|
-
|
|
787
845
|
{/* Lens (rule-based filtering) */}
|
|
788
846
|
<Tooltip>
|
|
789
847
|
<TooltipTrigger asChild>
|
|
@@ -793,6 +851,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
793
851
|
onClick={(e) => {
|
|
794
852
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
795
853
|
if (!lensPanelVisible) {
|
|
854
|
+
// Close other right-panel content first, then expand
|
|
855
|
+
setBcfPanelVisible(false);
|
|
856
|
+
setIdsPanelVisible(false);
|
|
796
857
|
setRightPanelCollapsed(false);
|
|
797
858
|
}
|
|
798
859
|
toggleLensPanel();
|