@ifc-lite/viewer 1.1.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
|
|
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
|
-
|
|
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-
|
|
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={() =>
|
|
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
|
-
<
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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="
|
|
314
|
-
<ToolButton tool="
|
|
315
|
-
<ToolButton tool="
|
|
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={
|
|
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={
|
|
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>
|
|
761
|
+
<TooltipContent>Info (?)</TooltipContent>
|
|
438
762
|
</Tooltip>
|
|
439
763
|
</div>
|
|
440
764
|
);
|