@ifc-lite/viewer 1.1.7 → 1.6.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 (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-BjDQoB2M.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-YBtrHPu3.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-CULtTDX3.js +111 -0
  15. package/dist/assets/wasm-bridge-CjL-lSak.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
@@ -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 { useRef, useCallback } from 'react';
5
+ import React, { useRef, useCallback, useMemo } from 'react';
6
6
  import {
7
7
  FolderOpen,
8
8
  Download,
@@ -29,6 +29,12 @@ import {
29
29
  Loader2,
30
30
  Camera,
31
31
  Info,
32
+ Layers,
33
+ SquareX,
34
+ Building2,
35
+ Plus,
36
+ MessageSquare,
37
+ ClipboardCheck,
32
38
  } from 'lucide-react';
33
39
  import { Button } from '@/components/ui/button';
34
40
  import { Separator } from '@/components/ui/separator';
@@ -37,17 +43,94 @@ import {
37
43
  DropdownMenu,
38
44
  DropdownMenuContent,
39
45
  DropdownMenuItem,
46
+ DropdownMenuCheckboxItem,
40
47
  DropdownMenuSeparator,
41
48
  DropdownMenuTrigger,
49
+ DropdownMenuSub,
50
+ DropdownMenuSubTrigger,
51
+ DropdownMenuSubContent,
42
52
  } from '@/components/ui/dropdown-menu';
43
53
  import { Progress } from '@/components/ui/progress';
44
- import { useViewerStore } from '@/store';
54
+ import { useViewerStore, isIfcxDataStore } from '@/store';
45
55
  import { useIfc } from '@/hooks/useIfc';
46
56
  import { cn } from '@/lib/utils';
47
57
  import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
48
- import { FileSpreadsheet, FileJson, SquareDashedMousePointer } from 'lucide-react';
58
+ import { FileSpreadsheet, FileJson, FileText, Filter, Upload, Pencil } from 'lucide-react';
59
+ import { ExportDialog } from './ExportDialog';
60
+ import { BulkPropertyEditor } from './BulkPropertyEditor';
61
+ import { DataConnector } from './DataConnector';
62
+ import { ExportChangesButton } from './ExportChangesButton';
63
+
64
+ type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
65
+
66
+ // #region FIX: Move ToolButton OUTSIDE MainToolbar to prevent recreation on every render
67
+ // This fixes Radix UI Tooltip's asChild prop becoming stale during re-renders
68
+ interface ToolButtonProps {
69
+ tool: Tool;
70
+ icon: React.ElementType;
71
+ label: string;
72
+ shortcut?: string;
73
+ activeTool: string;
74
+ onToolChange: (tool: Tool) => void;
75
+ }
76
+
77
+ function ToolButton({ tool, icon: Icon, label, shortcut, activeTool, onToolChange }: ToolButtonProps) {
78
+ return (
79
+ <Tooltip>
80
+ <TooltipTrigger asChild>
81
+ <Button
82
+ variant={activeTool === tool ? 'default' : 'ghost'}
83
+ size="icon-sm"
84
+ onClick={(e) => {
85
+ // Blur button to close tooltip after click
86
+ (e.currentTarget as HTMLButtonElement).blur();
87
+ onToolChange(tool);
88
+ }}
89
+ className={cn(activeTool === tool && 'bg-primary text-primary-foreground')}
90
+ >
91
+ <Icon className="h-4 w-4" />
92
+ </Button>
93
+ </TooltipTrigger>
94
+ <TooltipContent>
95
+ {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
96
+ </TooltipContent>
97
+ </Tooltip>
98
+ );
99
+ }
49
100
 
50
- type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section' | 'boxselect';
101
+ // #region FIX: Move ActionButton OUTSIDE MainToolbar to prevent recreation on every render
102
+ interface ActionButtonProps {
103
+ icon: React.ElementType;
104
+ label: string;
105
+ onClick: () => void;
106
+ shortcut?: string;
107
+ disabled?: boolean;
108
+ }
109
+
110
+ function ActionButton({ icon: Icon, label, onClick, shortcut, disabled }: ActionButtonProps) {
111
+ return (
112
+ <Tooltip>
113
+ <TooltipTrigger asChild>
114
+ <Button
115
+ variant="ghost"
116
+ size="icon-sm"
117
+ onClick={(e) => {
118
+ // Blur button to close tooltip after click
119
+ (e.currentTarget as HTMLButtonElement).blur();
120
+ onClick();
121
+ }}
122
+ disabled={disabled}
123
+ >
124
+ <Icon className="h-4 w-4" />
125
+ </Button>
126
+ </TooltipTrigger>
127
+ <TooltipContent>
128
+ {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
129
+ </TooltipContent>
130
+ </Tooltip>
131
+ );
132
+ }
133
+ // #endregion
51
134
 
52
135
  interface MainToolbarProps {
53
136
  onShowShortcuts?: () => void;
@@ -55,7 +138,11 @@ interface MainToolbarProps {
55
138
 
56
139
  export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainToolbarProps) {
57
140
  const fileInputRef = useRef<HTMLInputElement>(null);
58
- const { loadFile, loading, progress, geometryResult, ifcDataStore } = useIfc();
141
+ const addModelInputRef = useRef<HTMLInputElement>(null);
142
+ const { loadFile, loading, progress, geometryResult, ifcDataStore, models, clearAllModels, loadFilesSequentially, loadFederatedIfcx, addIfcxOverlays, addModel } = useIfc();
143
+
144
+ // Check if we have models loaded (for showing add model button)
145
+ const hasModelsLoaded = models.size > 0 || (geometryResult?.meshes && geometryResult.meshes.length > 0);
59
146
  const activeTool = useViewerStore((state) => state.activeTool);
60
147
  const setActiveTool = useViewerStore((state) => state.setActiveTool);
61
148
  const theme = useViewerStore((state) => state.theme);
@@ -64,17 +151,119 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
64
151
  const isolateEntity = useViewerStore((state) => state.isolateEntity);
65
152
  const hideEntity = useViewerStore((state) => state.hideEntity);
66
153
  const showAll = useViewerStore((state) => state.showAll);
154
+ const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
67
155
  const error = useViewerStore((state) => state.error);
68
156
  const cameraCallbacks = useViewerStore((state) => state.cameraCallbacks);
69
157
  const hoverTooltipsEnabled = useViewerStore((state) => state.hoverTooltipsEnabled);
70
158
  const toggleHoverTooltips = useViewerStore((state) => state.toggleHoverTooltips);
159
+ const typeVisibility = useViewerStore((state) => state.typeVisibility);
160
+ const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
161
+ const resetViewerState = useViewerStore((state) => state.resetViewerState);
162
+ const bcfPanelVisible = useViewerStore((state) => state.bcfPanelVisible);
163
+ const toggleBcfPanel = useViewerStore((state) => state.toggleBcfPanel);
164
+ const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
165
+ const toggleIdsPanel = useViewerStore((state) => state.toggleIdsPanel);
166
+ const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
167
+
168
+ // Check which type geometries exist across ALL loaded models (federation-aware)
169
+ const typeGeometryExists = useMemo(() => {
170
+ const result = { spaces: false, openings: false, site: false };
171
+
172
+ // Check all federated models
173
+ if (models.size > 0) {
174
+ for (const [, model] of models) {
175
+ const meshes = model.geometryResult?.meshes;
176
+ if (!meshes) continue;
177
+ for (const m of meshes) {
178
+ if (m.ifcType === 'IfcSpace') result.spaces = true;
179
+ else if (m.ifcType === 'IfcOpeningElement') result.openings = true;
180
+ else if (m.ifcType === 'IfcSite') result.site = true;
181
+ // Early exit if all found
182
+ if (result.spaces && result.openings && result.site) return result;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Fallback: also check legacy single-model geometryResult
188
+ if (geometryResult?.meshes) {
189
+ for (const m of geometryResult.meshes) {
190
+ if (m.ifcType === 'IfcSpace') result.spaces = true;
191
+ else if (m.ifcType === 'IfcOpeningElement') result.openings = true;
192
+ else if (m.ifcType === 'IfcSite') result.site = true;
193
+ if (result.spaces && result.openings && result.site) return result;
194
+ }
195
+ }
196
+
197
+ return result;
198
+ }, [models, geometryResult]);
71
199
 
72
200
  const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
73
- const file = e.target.files?.[0];
74
- if (file) {
75
- loadFile(file);
201
+ const files = e.target.files;
202
+ if (!files || files.length === 0) return;
203
+
204
+ // Filter to supported files (IFC, IFCX, GLB)
205
+ const supportedFiles = Array.from(files).filter(
206
+ f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
207
+ );
208
+
209
+ if (supportedFiles.length === 0) return;
210
+
211
+ if (supportedFiles.length === 1) {
212
+ // Single file - use loadFile (simpler single-model path)
213
+ loadFile(supportedFiles[0]);
214
+ } else {
215
+ // Multiple files - check if ALL are IFCX (use federated loading for layer composition)
216
+ const allIfcx = supportedFiles.every(f => f.name.endsWith('.ifcx'));
217
+
218
+ resetViewerState();
219
+ clearAllModels();
220
+
221
+ if (allIfcx) {
222
+ // IFCX files use federated loading (layer composition - later files override earlier ones)
223
+ // This handles overlay files that add properties without geometry
224
+ console.log(`[MainToolbar] Loading ${supportedFiles.length} IFCX files with federated composition`);
225
+ loadFederatedIfcx(supportedFiles);
226
+ } else {
227
+ // Mixed or all IFC4/GLB files - load sequentially as independent models
228
+ loadFilesSequentially(supportedFiles);
229
+ }
230
+ }
231
+
232
+ // Reset input so same files can be selected again
233
+ e.target.value = '';
234
+ }, [loadFile, loadFilesSequentially, loadFederatedIfcx, resetViewerState, clearAllModels]);
235
+
236
+ const handleAddModelSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
237
+ const files = e.target.files;
238
+ if (!files || files.length === 0) return;
239
+
240
+ // Filter to supported files (IFC, IFCX, GLB)
241
+ const supportedFiles = Array.from(files).filter(
242
+ f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
243
+ );
244
+
245
+ if (supportedFiles.length === 0) return;
246
+
247
+ // Check if adding IFCX files
248
+ const newFilesAreIfcx = supportedFiles.every(f => f.name.endsWith('.ifcx'));
249
+ const existingIsIfcx = isIfcxDataStore(ifcDataStore);
250
+
251
+ if (newFilesAreIfcx && existingIsIfcx) {
252
+ // Adding IFCX overlay(s) to existing IFCX model - re-compose with new layers
253
+ console.log(`[MainToolbar] Adding ${supportedFiles.length} IFCX overlay(s) to existing IFCX model - re-composing`);
254
+ addIfcxOverlays(supportedFiles);
255
+ } else if (newFilesAreIfcx && !existingIsIfcx && ifcDataStore) {
256
+ // User trying to add IFCX to IFC4 model - won't work
257
+ console.warn('[MainToolbar] Cannot add IFCX files to non-IFCX model');
258
+ alert(`IFCX overlay files cannot be added to IFC4 models.\n\nPlease load IFCX files separately.`);
259
+ } else {
260
+ // Standard case - add as independent models (IFC4, GLB, or mixed)
261
+ loadFilesSequentially(supportedFiles);
76
262
  }
77
- }, [loadFile]);
263
+
264
+ // Reset input so same files can be selected again
265
+ e.target.value = '';
266
+ }, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
78
267
 
79
268
  const handleIsolate = useCallback(() => {
80
269
  if (selectedEntityId) {
@@ -82,11 +271,20 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
82
271
  }
83
272
  }, [selectedEntityId, isolateEntity]);
84
273
 
274
+ const clearSelection = useViewerStore((state) => state.clearSelection);
275
+
85
276
  const handleHide = useCallback(() => {
86
277
  if (selectedEntityId) {
87
278
  hideEntity(selectedEntityId);
279
+ // Clear selection after hiding - element is no longer visible
280
+ clearSelection();
88
281
  }
89
- }, [selectedEntityId, hideEntity]);
282
+ }, [selectedEntityId, hideEntity, clearSelection]);
283
+
284
+ const handleShowAll = useCallback(() => {
285
+ showAll();
286
+ clearStoreySelection(); // Also clear storey filtering (matches 'A' keyboard shortcut)
287
+ }, [showAll, clearStoreySelection]);
90
288
 
91
289
  const handleExportGLB = useCallback(() => {
92
290
  if (!geometryResult) return;
@@ -120,7 +318,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
120
318
  }
121
319
  }, []);
122
320
 
123
- const handleExportCSV = useCallback((type: 'entities' | 'properties' | 'quantities') => {
321
+ const handleExportCSV = useCallback((type: 'entities' | 'properties' | 'quantities' | 'spatial') => {
124
322
  if (!ifcDataStore) return;
125
323
  try {
126
324
  const exporter = new CSVExporter(ifcDataStore);
@@ -140,6 +338,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
140
338
  csv = exporter.exportQuantities();
141
339
  filename = 'quantities.csv';
142
340
  break;
341
+ case 'spatial':
342
+ csv = exporter.exportSpatialHierarchy();
343
+ filename = 'spatial-hierarchy.csv';
344
+ break;
143
345
  }
144
346
 
145
347
  const blob = new Blob([csv], { type: 'text/csv' });
@@ -183,81 +385,36 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
183
385
  }
184
386
  }, [ifcDataStore]);
185
387
 
186
- const ToolButton = ({
187
- tool,
188
- icon: Icon,
189
- label,
190
- shortcut
191
- }: {
192
- tool: Tool;
193
- icon: React.ElementType;
194
- label: string;
195
- shortcut?: string;
196
- }) => (
197
- <Tooltip>
198
- <TooltipTrigger asChild>
199
- <Button
200
- variant={activeTool === tool ? 'default' : 'ghost'}
201
- size="icon-sm"
202
- onClick={() => setActiveTool(tool)}
203
- className={cn(activeTool === tool && 'bg-primary text-primary-foreground')}
204
- >
205
- <Icon className="h-4 w-4" />
206
- </Button>
207
- </TooltipTrigger>
208
- <TooltipContent>
209
- {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
210
- </TooltipContent>
211
- </Tooltip>
212
- );
213
-
214
- const ActionButton = ({
215
- icon: Icon,
216
- label,
217
- onClick,
218
- shortcut,
219
- disabled
220
- }: {
221
- icon: React.ElementType;
222
- label: string;
223
- onClick: () => void;
224
- shortcut?: string;
225
- disabled?: boolean;
226
- }) => (
227
- <Tooltip>
228
- <TooltipTrigger asChild>
229
- <Button
230
- variant="ghost"
231
- size="icon-sm"
232
- onClick={onClick}
233
- disabled={disabled}
234
- >
235
- <Icon className="h-4 w-4" />
236
- </Button>
237
- </TooltipTrigger>
238
- <TooltipContent>
239
- {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
240
- </TooltipContent>
241
- </Tooltip>
242
- );
243
-
244
388
  return (
245
- <div className="flex items-center gap-1 px-2 h-12 border-b bg-card">
389
+ <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">
246
390
  {/* File Operations */}
247
391
  <input
248
392
  ref={fileInputRef}
249
393
  type="file"
250
- accept=".ifc"
394
+ accept=".ifc,.ifcx,.glb"
395
+ multiple
251
396
  onChange={handleFileSelect}
252
397
  className="hidden"
253
398
  />
399
+ <input
400
+ ref={addModelInputRef}
401
+ type="file"
402
+ accept=".ifc,.ifcx,.glb"
403
+ multiple
404
+ onChange={handleAddModelSelect}
405
+ className="hidden"
406
+ />
254
407
 
255
408
  <Tooltip>
256
409
  <TooltipTrigger asChild>
257
410
  <Button
258
411
  variant="ghost"
259
412
  size="icon-sm"
260
- onClick={() => fileInputRef.current?.click()}
413
+ onClick={(e) => {
414
+ // Blur button to close tooltip before opening file dialog
415
+ (e.currentTarget as HTMLButtonElement).blur();
416
+ fileInputRef.current?.click();
417
+ }}
261
418
  disabled={loading}
262
419
  >
263
420
  {loading ? (
@@ -270,6 +427,27 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
270
427
  <TooltipContent>Open IFC File</TooltipContent>
271
428
  </Tooltip>
272
429
 
430
+ {/* Add Model button - only shown when models are loaded */}
431
+ {hasModelsLoaded && (
432
+ <Tooltip>
433
+ <TooltipTrigger asChild>
434
+ <Button
435
+ variant="ghost"
436
+ size="icon-sm"
437
+ onClick={(e) => {
438
+ (e.currentTarget as HTMLButtonElement).blur();
439
+ addModelInputRef.current?.click();
440
+ }}
441
+ disabled={loading}
442
+ className="text-[#9ece6a] hover:text-[#9ece6a] hover:bg-[#9ece6a]/10"
443
+ >
444
+ <Plus className="h-4 w-4" />
445
+ </Button>
446
+ </TooltipTrigger>
447
+ <TooltipContent>Add Model to Scene (Multi-select supported)</TooltipContent>
448
+ </Tooltip>
449
+ )}
450
+
273
451
  <DropdownMenu>
274
452
  <DropdownMenuTrigger asChild>
275
453
  <Button variant="ghost" size="icon-sm" disabled={!geometryResult}>
@@ -277,23 +455,45 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
277
455
  </Button>
278
456
  </DropdownMenuTrigger>
279
457
  <DropdownMenuContent>
458
+ <ExportDialog
459
+ trigger={
460
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
461
+ <FileText className="h-4 w-4 mr-2" />
462
+ Export IFC (with changes)
463
+ </DropdownMenuItem>
464
+ }
465
+ />
466
+ <DropdownMenuSeparator />
280
467
  <DropdownMenuItem onClick={handleExportGLB}>
281
468
  <Download className="h-4 w-4 mr-2" />
282
469
  Export GLB (3D Model)
283
470
  </DropdownMenuItem>
284
471
  <DropdownMenuSeparator />
285
- <DropdownMenuItem onClick={() => handleExportCSV('entities')} disabled={!ifcDataStore}>
286
- <FileSpreadsheet className="h-4 w-4 mr-2" />
287
- Export Entities (CSV)
288
- </DropdownMenuItem>
289
- <DropdownMenuItem onClick={() => handleExportCSV('properties')} disabled={!ifcDataStore}>
290
- <FileSpreadsheet className="h-4 w-4 mr-2" />
291
- Export Properties (CSV)
292
- </DropdownMenuItem>
293
- <DropdownMenuItem onClick={() => handleExportCSV('quantities')} disabled={!ifcDataStore}>
294
- <FileSpreadsheet className="h-4 w-4 mr-2" />
295
- Export Quantities (CSV)
296
- </DropdownMenuItem>
472
+ <DropdownMenuSub>
473
+ <DropdownMenuSubTrigger disabled={!ifcDataStore}>
474
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
475
+ Export CSV
476
+ </DropdownMenuSubTrigger>
477
+ <DropdownMenuSubContent>
478
+ <DropdownMenuItem onClick={() => handleExportCSV('entities')}>
479
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
480
+ Entities
481
+ </DropdownMenuItem>
482
+ <DropdownMenuItem onClick={() => handleExportCSV('properties')}>
483
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
484
+ Properties
485
+ </DropdownMenuItem>
486
+ <DropdownMenuItem onClick={() => handleExportCSV('quantities')}>
487
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
488
+ Quantities
489
+ </DropdownMenuItem>
490
+ <DropdownMenuSeparator />
491
+ <DropdownMenuItem onClick={() => handleExportCSV('spatial')}>
492
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
493
+ Spatial Hierarchy
494
+ </DropdownMenuItem>
495
+ </DropdownMenuSubContent>
496
+ </DropdownMenuSub>
297
497
  <DropdownMenuItem onClick={handleExportJSON} disabled={!ifcDataStore}>
298
498
  <FileJson className="h-4 w-4 mr-2" />
299
499
  Export JSON (All Data)
@@ -306,27 +506,147 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
306
506
  </DropdownMenuContent>
307
507
  </DropdownMenu>
308
508
 
509
+ {/* Edit Menu - Bulk editing and data import */}
510
+ <DropdownMenu>
511
+ <Tooltip>
512
+ <TooltipTrigger asChild>
513
+ <DropdownMenuTrigger asChild>
514
+ <Button variant="ghost" size="icon-sm" disabled={!ifcDataStore}>
515
+ <Pencil className="h-4 w-4" />
516
+ </Button>
517
+ </DropdownMenuTrigger>
518
+ </TooltipTrigger>
519
+ <TooltipContent>Edit Properties</TooltipContent>
520
+ </Tooltip>
521
+ <DropdownMenuContent>
522
+ <BulkPropertyEditor
523
+ trigger={
524
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
525
+ <Filter className="h-4 w-4 mr-2" />
526
+ Bulk Property Editor
527
+ </DropdownMenuItem>
528
+ }
529
+ />
530
+ <DataConnector
531
+ trigger={
532
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
533
+ <Upload className="h-4 w-4 mr-2" />
534
+ Import Data (CSV)
535
+ </DropdownMenuItem>
536
+ }
537
+ />
538
+ </DropdownMenuContent>
539
+ </DropdownMenu>
540
+
541
+ {/* Export Changes Button - shows when there are pending mutations */}
542
+ <ExportChangesButton />
543
+
544
+ {/* BCF Issues Button */}
545
+ <Tooltip>
546
+ <TooltipTrigger asChild>
547
+ <Button
548
+ variant={bcfPanelVisible ? 'default' : 'ghost'}
549
+ size="icon-sm"
550
+ onClick={(e) => {
551
+ (e.currentTarget as HTMLButtonElement).blur();
552
+ // If BCF is being shown, also expand the right panel
553
+ if (!bcfPanelVisible) {
554
+ setRightPanelCollapsed(false);
555
+ }
556
+ toggleBcfPanel();
557
+ }}
558
+ className={cn(bcfPanelVisible && 'bg-primary text-primary-foreground')}
559
+ >
560
+ <MessageSquare className="h-4 w-4" />
561
+ </Button>
562
+ </TooltipTrigger>
563
+ <TooltipContent>BCF Issues</TooltipContent>
564
+ </Tooltip>
565
+
566
+ {/* IDS Validation Button */}
567
+ <Tooltip>
568
+ <TooltipTrigger asChild>
569
+ <Button
570
+ variant={idsPanelVisible ? 'default' : 'ghost'}
571
+ size="icon-sm"
572
+ onClick={(e) => {
573
+ (e.currentTarget as HTMLButtonElement).blur();
574
+ // If IDS is being shown, also expand the right panel
575
+ if (!idsPanelVisible) {
576
+ setRightPanelCollapsed(false);
577
+ }
578
+ toggleIdsPanel();
579
+ }}
580
+ className={cn(idsPanelVisible && 'bg-primary text-primary-foreground')}
581
+ >
582
+ <ClipboardCheck className="h-4 w-4" />
583
+ </Button>
584
+ </TooltipTrigger>
585
+ <TooltipContent>IDS Validation</TooltipContent>
586
+ </Tooltip>
587
+
309
588
  <Separator orientation="vertical" className="h-6 mx-1" />
310
589
 
311
590
  {/* Navigation Tools */}
312
- <ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" />
313
- <ToolButton tool="boxselect" icon={SquareDashedMousePointer} label="Box Select" shortcut="B" />
314
- <ToolButton tool="pan" icon={Hand} label="Pan" shortcut="P" />
315
- <ToolButton tool="orbit" icon={Rotate3d} label="Orbit" shortcut="O" />
316
- <ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" />
591
+ <ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" activeTool={activeTool} onToolChange={setActiveTool} />
592
+ <ToolButton tool="pan" icon={Hand} label="Pan" shortcut="P" activeTool={activeTool} onToolChange={setActiveTool} />
593
+ <ToolButton tool="orbit" icon={Rotate3d} label="Orbit" shortcut="O" activeTool={activeTool} onToolChange={setActiveTool} />
594
+ <ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" activeTool={activeTool} onToolChange={setActiveTool} />
317
595
 
318
596
  <Separator orientation="vertical" className="h-6 mx-1" />
319
597
 
320
598
  {/* Measurement & Section */}
321
- <ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" />
322
- <ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" />
599
+ <ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" activeTool={activeTool} onToolChange={setActiveTool} />
600
+ <ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" activeTool={activeTool} onToolChange={setActiveTool} />
323
601
 
324
602
  <Separator orientation="vertical" className="h-6 mx-1" />
325
603
 
326
604
  {/* Visibility */}
327
605
  <ActionButton icon={Focus} label="Isolate Selection" onClick={handleIsolate} shortcut="I" disabled={!selectedEntityId} />
328
606
  <ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del" disabled={!selectedEntityId} />
329
- <ActionButton icon={Eye} label="Show All" onClick={showAll} shortcut="A" />
607
+ <ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
608
+
609
+ <DropdownMenu>
610
+ <Tooltip>
611
+ <TooltipTrigger asChild>
612
+ <DropdownMenuTrigger asChild>
613
+ <Button variant="ghost" size="icon-sm" disabled={!geometryResult && models.size === 0}>
614
+ <Layers className="h-4 w-4" />
615
+ </Button>
616
+ </DropdownMenuTrigger>
617
+ </TooltipTrigger>
618
+ <TooltipContent>Type Visibility</TooltipContent>
619
+ </Tooltip>
620
+ <DropdownMenuContent>
621
+ {typeGeometryExists.spaces && (
622
+ <DropdownMenuCheckboxItem
623
+ checked={typeVisibility.spaces}
624
+ onCheckedChange={() => toggleTypeVisibility('spaces')}
625
+ >
626
+ <Box className="h-4 w-4 mr-2" style={{ color: '#33d9ff' }} />
627
+ Show Spaces
628
+ </DropdownMenuCheckboxItem>
629
+ )}
630
+ {typeGeometryExists.openings && (
631
+ <DropdownMenuCheckboxItem
632
+ checked={typeVisibility.openings}
633
+ onCheckedChange={() => toggleTypeVisibility('openings')}
634
+ >
635
+ <SquareX className="h-4 w-4 mr-2" style={{ color: '#ff6b4a' }} />
636
+ Show Openings
637
+ </DropdownMenuCheckboxItem>
638
+ )}
639
+ {typeGeometryExists.site && (
640
+ <DropdownMenuCheckboxItem
641
+ checked={typeVisibility.site}
642
+ onCheckedChange={() => toggleTypeVisibility('site')}
643
+ >
644
+ <Building2 className="h-4 w-4 mr-2" style={{ color: '#66cc4d' }} />
645
+ Show Site
646
+ </DropdownMenuCheckboxItem>
647
+ )}
648
+ </DropdownMenuContent>
649
+ </DropdownMenu>
330
650
 
331
651
  <Separator orientation="vertical" className="h-6 mx-1" />
332
652
 
@@ -336,7 +656,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
336
656
  <Button
337
657
  variant={hoverTooltipsEnabled ? 'default' : 'ghost'}
338
658
  size="icon-sm"
339
- onClick={toggleHoverTooltips}
659
+ onClick={(e) => {
660
+ // Blur button to close tooltip after click
661
+ (e.currentTarget as HTMLButtonElement).blur();
662
+ toggleHoverTooltips();
663
+ }}
340
664
  className={cn(hoverTooltipsEnabled && 'bg-primary text-primary-foreground')}
341
665
  >
342
666
  <Info className="h-4 w-4" />
@@ -434,7 +758,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
434
758
  <HelpCircle className="h-4 w-4" />
435
759
  </Button>
436
760
  </TooltipTrigger>
437
- <TooltipContent>Keyboard Shortcuts (?)</TooltipContent>
761
+ <TooltipContent>Info (?)</TooltipContent>
438
762
  </Tooltip>
439
763
  </div>
440
764
  );