@ifc-lite/viewer 1.1.6 → 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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dedicated button for exporting IFC with property mutations applied.
|
|
7
|
+
* Shows when there are pending changes and provides one-click export.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
11
|
+
import { Download, Loader2, Check, AlertCircle } from 'lucide-react';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Badge } from '@/components/ui/badge';
|
|
14
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
15
|
+
import { useViewerStore } from '@/store';
|
|
16
|
+
import { StepExporter } from '@ifc-lite/export';
|
|
17
|
+
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
18
|
+
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
19
|
+
|
|
20
|
+
interface ExportChangesButtonProps {
|
|
21
|
+
/** Optional custom class name */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ExportChangesButton({ className }: ExportChangesButtonProps) {
|
|
26
|
+
const models = useViewerStore((s) => s.models);
|
|
27
|
+
const getMutationView = useViewerStore((s) => s.getMutationView);
|
|
28
|
+
const registerMutationView = useViewerStore((s) => s.registerMutationView);
|
|
29
|
+
const mutationVersion = useViewerStore((s) => s.mutationVersion);
|
|
30
|
+
|
|
31
|
+
// Legacy single-model support
|
|
32
|
+
const legacyIfcDataStore = useViewerStore((s) => s.ifcDataStore);
|
|
33
|
+
const legacyGeometryResult = useViewerStore((s) => s.geometryResult);
|
|
34
|
+
|
|
35
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
36
|
+
const [exportStatus, setExportStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
37
|
+
|
|
38
|
+
// Get model info - supports both federated models and legacy single-model
|
|
39
|
+
const modelInfo = useMemo(() => {
|
|
40
|
+
// First check federated models
|
|
41
|
+
if (models.size > 0) {
|
|
42
|
+
const firstModel = models.values().next().value;
|
|
43
|
+
if (firstModel) {
|
|
44
|
+
return {
|
|
45
|
+
id: firstModel.id,
|
|
46
|
+
name: firstModel.name,
|
|
47
|
+
ifcDataStore: firstModel.ifcDataStore,
|
|
48
|
+
schemaVersion: firstModel.schemaVersion,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Fall back to legacy single-model
|
|
53
|
+
if (legacyIfcDataStore && legacyGeometryResult) {
|
|
54
|
+
return {
|
|
55
|
+
id: '__legacy__',
|
|
56
|
+
name: 'model',
|
|
57
|
+
ifcDataStore: legacyIfcDataStore,
|
|
58
|
+
schemaVersion: legacyIfcDataStore.schemaVersion,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}, [models, legacyIfcDataStore, legacyGeometryResult]);
|
|
63
|
+
|
|
64
|
+
// Count mutations
|
|
65
|
+
const mutationCount = useMemo(() => {
|
|
66
|
+
if (!modelInfo) return 0;
|
|
67
|
+
const mutationView = getMutationView(modelInfo.id);
|
|
68
|
+
return mutationView?.getMutations().length || 0;
|
|
69
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, [modelInfo, getMutationView, mutationVersion]);
|
|
71
|
+
|
|
72
|
+
// Ensure mutation view exists
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!modelInfo?.ifcDataStore) return;
|
|
75
|
+
|
|
76
|
+
let mutationView = getMutationView(modelInfo.id);
|
|
77
|
+
if (mutationView) return;
|
|
78
|
+
|
|
79
|
+
const dataStore = modelInfo.ifcDataStore;
|
|
80
|
+
mutationView = new MutablePropertyView(dataStore.properties || null, modelInfo.id);
|
|
81
|
+
|
|
82
|
+
// Set up on-demand property extraction
|
|
83
|
+
if (dataStore.onDemandPropertyMap && dataStore.source?.length > 0) {
|
|
84
|
+
mutationView.setOnDemandExtractor((entityId: number) => {
|
|
85
|
+
return extractPropertiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
registerMutationView(modelInfo.id, mutationView);
|
|
90
|
+
}, [modelInfo, getMutationView, registerMutationView]);
|
|
91
|
+
|
|
92
|
+
// Format date as YYYY-MM-DD
|
|
93
|
+
const formatDate = useCallback(() => {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const year = now.getFullYear();
|
|
96
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
97
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
98
|
+
return `${year}-${month}-${day}`;
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
// Generate filename from model name + date
|
|
102
|
+
const generateFilename = useCallback(() => {
|
|
103
|
+
if (!modelInfo) return 'export.ifc';
|
|
104
|
+
// Remove extension if present
|
|
105
|
+
const baseName = modelInfo.name.replace(/\.[^.]+$/, '');
|
|
106
|
+
return `${baseName}_${formatDate()}.ifc`;
|
|
107
|
+
}, [modelInfo, formatDate]);
|
|
108
|
+
|
|
109
|
+
const handleExport = useCallback(async () => {
|
|
110
|
+
if (!modelInfo) return;
|
|
111
|
+
|
|
112
|
+
setIsExporting(true);
|
|
113
|
+
setExportStatus('idle');
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const mutationView = getMutationView(modelInfo.id);
|
|
117
|
+
|
|
118
|
+
// Determine schema version
|
|
119
|
+
const schemaVersion = modelInfo.schemaVersion || 'IFC4';
|
|
120
|
+
const schema = schemaVersion.includes('2X3') ? 'IFC2X3'
|
|
121
|
+
: schemaVersion.includes('4X3') ? 'IFC4X3'
|
|
122
|
+
: 'IFC4';
|
|
123
|
+
|
|
124
|
+
const exporter = new StepExporter(modelInfo.ifcDataStore, mutationView || undefined);
|
|
125
|
+
const result = exporter.export({
|
|
126
|
+
schema: schema as 'IFC2X3' | 'IFC4' | 'IFC4X3',
|
|
127
|
+
includeGeometry: true,
|
|
128
|
+
applyMutations: true,
|
|
129
|
+
deltaOnly: false,
|
|
130
|
+
description: `Exported from ifc-lite with ${mutationCount} modifications`,
|
|
131
|
+
application: 'ifc-lite',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Download the file
|
|
135
|
+
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
136
|
+
const url = URL.createObjectURL(blob);
|
|
137
|
+
const a = document.createElement('a');
|
|
138
|
+
a.href = url;
|
|
139
|
+
a.download = generateFilename();
|
|
140
|
+
document.body.appendChild(a);
|
|
141
|
+
a.click();
|
|
142
|
+
document.body.removeChild(a);
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
|
|
145
|
+
setExportStatus('success');
|
|
146
|
+
|
|
147
|
+
// Reset status after 2 seconds
|
|
148
|
+
setTimeout(() => setExportStatus('idle'), 2000);
|
|
149
|
+
|
|
150
|
+
console.log(`[ExportChangesButton] Exported ${result.stats.entityCount} entities (${result.stats.modifiedEntityCount} modified)`);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[ExportChangesButton] Export failed:', error);
|
|
153
|
+
setExportStatus('error');
|
|
154
|
+
setTimeout(() => setExportStatus('idle'), 3000);
|
|
155
|
+
} finally {
|
|
156
|
+
setIsExporting(false);
|
|
157
|
+
}
|
|
158
|
+
}, [modelInfo, getMutationView, mutationCount, generateFilename]);
|
|
159
|
+
|
|
160
|
+
// Don't render if no model or no mutations
|
|
161
|
+
if (!modelInfo || mutationCount === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Tooltip>
|
|
167
|
+
<TooltipTrigger asChild>
|
|
168
|
+
<Button
|
|
169
|
+
variant="outline"
|
|
170
|
+
size="sm"
|
|
171
|
+
onClick={handleExport}
|
|
172
|
+
disabled={isExporting}
|
|
173
|
+
className={className}
|
|
174
|
+
>
|
|
175
|
+
{isExporting ? (
|
|
176
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
177
|
+
) : exportStatus === 'success' ? (
|
|
178
|
+
<Check className="h-4 w-4 mr-2 text-green-500" />
|
|
179
|
+
) : exportStatus === 'error' ? (
|
|
180
|
+
<AlertCircle className="h-4 w-4 mr-2 text-red-500" />
|
|
181
|
+
) : (
|
|
182
|
+
<Download className="h-4 w-4 mr-2" />
|
|
183
|
+
)}
|
|
184
|
+
Export Changes
|
|
185
|
+
<Badge variant="secondary" className="ml-2 text-xs">
|
|
186
|
+
{mutationCount}
|
|
187
|
+
</Badge>
|
|
188
|
+
</Button>
|
|
189
|
+
</TooltipTrigger>
|
|
190
|
+
<TooltipContent>
|
|
191
|
+
Export IFC with {mutationCount} property changes applied
|
|
192
|
+
</TooltipContent>
|
|
193
|
+
</Tooltip>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Export Dialog for IFC export with property mutations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Download,
|
|
12
|
+
FileText,
|
|
13
|
+
FileJson,
|
|
14
|
+
AlertCircle,
|
|
15
|
+
Check,
|
|
16
|
+
Loader2,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { Button } from '@/components/ui/button';
|
|
19
|
+
import { Label } from '@/components/ui/label';
|
|
20
|
+
import { Switch } from '@/components/ui/switch';
|
|
21
|
+
import { Badge } from '@/components/ui/badge';
|
|
22
|
+
import {
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
} from '@/components/ui/select';
|
|
29
|
+
import {
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogContent,
|
|
32
|
+
DialogDescription,
|
|
33
|
+
DialogFooter,
|
|
34
|
+
DialogHeader,
|
|
35
|
+
DialogTitle,
|
|
36
|
+
DialogTrigger,
|
|
37
|
+
} from '@/components/ui/dialog';
|
|
38
|
+
import {
|
|
39
|
+
Alert,
|
|
40
|
+
AlertDescription,
|
|
41
|
+
AlertTitle,
|
|
42
|
+
} from '@/components/ui/alert';
|
|
43
|
+
import { useViewerStore } from '@/store';
|
|
44
|
+
import { StepExporter } from '@ifc-lite/export';
|
|
45
|
+
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
46
|
+
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
47
|
+
|
|
48
|
+
type ExportFormat = 'ifc' | 'ifcx' | 'json';
|
|
49
|
+
type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3';
|
|
50
|
+
|
|
51
|
+
interface ExportDialogProps {
|
|
52
|
+
trigger?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
56
|
+
const models = useViewerStore((s) => s.models);
|
|
57
|
+
const dirtyModels = useViewerStore((s) => s.dirtyModels);
|
|
58
|
+
const getMutationView = useViewerStore((s) => s.getMutationView);
|
|
59
|
+
const registerMutationView = useViewerStore((s) => s.registerMutationView);
|
|
60
|
+
const getModifiedEntityCount = useViewerStore((s) => s.getModifiedEntityCount);
|
|
61
|
+
// Also get legacy single-model state for backward compatibility
|
|
62
|
+
const legacyIfcDataStore = useViewerStore((s) => s.ifcDataStore);
|
|
63
|
+
const legacyGeometryResult = useViewerStore((s) => s.geometryResult);
|
|
64
|
+
|
|
65
|
+
const [open, setOpen] = useState(false);
|
|
66
|
+
const [format, setFormat] = useState<ExportFormat>('ifc');
|
|
67
|
+
const [schema, setSchema] = useState<SchemaVersion>('IFC4');
|
|
68
|
+
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
|
69
|
+
const [includeGeometry, setIncludeGeometry] = useState(true);
|
|
70
|
+
const [applyMutations, setApplyMutations] = useState(true);
|
|
71
|
+
const [deltaOnly, setDeltaOnly] = useState(false);
|
|
72
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
73
|
+
const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
74
|
+
|
|
75
|
+
// Get list of models with data stores - includes both federated models and legacy single-model
|
|
76
|
+
const modelList = useMemo(() => {
|
|
77
|
+
const list = Array.from(models.values()).map((m) => ({
|
|
78
|
+
id: m.id,
|
|
79
|
+
name: m.name,
|
|
80
|
+
isDirty: dirtyModels.has(m.id),
|
|
81
|
+
schemaVersion: m.schemaVersion,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// If no models in Map but legacy data exists, add a synthetic entry
|
|
85
|
+
if (list.length === 0 && legacyIfcDataStore) {
|
|
86
|
+
list.push({
|
|
87
|
+
id: '__legacy__',
|
|
88
|
+
name: 'Current Model',
|
|
89
|
+
isDirty: false,
|
|
90
|
+
schemaVersion: legacyIfcDataStore.schemaVersion,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return list;
|
|
95
|
+
}, [models, dirtyModels, legacyIfcDataStore]);
|
|
96
|
+
|
|
97
|
+
// Select first model by default
|
|
98
|
+
useMemo(() => {
|
|
99
|
+
if (modelList.length > 0 && !selectedModelId) {
|
|
100
|
+
setSelectedModelId(modelList[0].id);
|
|
101
|
+
}
|
|
102
|
+
}, [modelList, selectedModelId]);
|
|
103
|
+
|
|
104
|
+
// Get selected model's data - supports both federated and legacy mode
|
|
105
|
+
const selectedModel = useMemo(() => {
|
|
106
|
+
if (selectedModelId === '__legacy__' && legacyIfcDataStore && legacyGeometryResult) {
|
|
107
|
+
// Return a synthetic FederatedModel-like object for legacy mode
|
|
108
|
+
return {
|
|
109
|
+
id: '__legacy__',
|
|
110
|
+
name: 'Current Model',
|
|
111
|
+
ifcDataStore: legacyIfcDataStore,
|
|
112
|
+
geometryResult: legacyGeometryResult,
|
|
113
|
+
visible: true,
|
|
114
|
+
collapsed: false,
|
|
115
|
+
schemaVersion: legacyIfcDataStore.schemaVersion,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return models.get(selectedModelId);
|
|
119
|
+
}, [models, selectedModelId, legacyIfcDataStore, legacyGeometryResult]);
|
|
120
|
+
|
|
121
|
+
// Ensure mutation view exists for selected model
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!selectedModel?.ifcDataStore || !selectedModelId) return;
|
|
124
|
+
|
|
125
|
+
// Check if mutation view already exists
|
|
126
|
+
let mutationView = getMutationView(selectedModelId);
|
|
127
|
+
if (mutationView) return;
|
|
128
|
+
|
|
129
|
+
// Create new mutation view with on-demand property extractor
|
|
130
|
+
const dataStore = selectedModel.ifcDataStore;
|
|
131
|
+
mutationView = new MutablePropertyView(dataStore.properties || null, selectedModelId);
|
|
132
|
+
|
|
133
|
+
// Set up on-demand property extraction if the data store supports it
|
|
134
|
+
if (dataStore.onDemandPropertyMap && dataStore.source?.length > 0) {
|
|
135
|
+
mutationView.setOnDemandExtractor((entityId: number) => {
|
|
136
|
+
return extractPropertiesOnDemand(dataStore as IfcDataStore, entityId);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Register the mutation view
|
|
141
|
+
registerMutationView(selectedModelId, mutationView);
|
|
142
|
+
}, [selectedModel, selectedModelId, getMutationView, registerMutationView]);
|
|
143
|
+
|
|
144
|
+
const modifiedCount = useMemo(() => {
|
|
145
|
+
return getModifiedEntityCount();
|
|
146
|
+
}, [getModifiedEntityCount]);
|
|
147
|
+
|
|
148
|
+
const handleExport = useCallback(async () => {
|
|
149
|
+
if (!selectedModel) return;
|
|
150
|
+
|
|
151
|
+
setIsExporting(true);
|
|
152
|
+
setExportResult(null);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const mutationView = getMutationView(selectedModelId);
|
|
156
|
+
|
|
157
|
+
if (format === 'ifc') {
|
|
158
|
+
const exporter = new StepExporter(selectedModel.ifcDataStore, mutationView || undefined);
|
|
159
|
+
const result = exporter.export({
|
|
160
|
+
schema,
|
|
161
|
+
includeGeometry,
|
|
162
|
+
applyMutations,
|
|
163
|
+
deltaOnly,
|
|
164
|
+
description: `Exported from ifc-lite with ${modifiedCount} modifications`,
|
|
165
|
+
application: 'ifc-lite',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Download the file
|
|
169
|
+
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
170
|
+
const url = URL.createObjectURL(blob);
|
|
171
|
+
const a = document.createElement('a');
|
|
172
|
+
a.href = url;
|
|
173
|
+
a.download = `${selectedModel.name.replace(/\.[^.]+$/, '')}_modified.ifc`;
|
|
174
|
+
document.body.appendChild(a);
|
|
175
|
+
a.click();
|
|
176
|
+
document.body.removeChild(a);
|
|
177
|
+
URL.revokeObjectURL(url);
|
|
178
|
+
|
|
179
|
+
setExportResult({
|
|
180
|
+
success: true,
|
|
181
|
+
message: `Exported ${result.stats.entityCount} entities (${result.stats.modifiedEntityCount} modified)`,
|
|
182
|
+
});
|
|
183
|
+
} else if (format === 'ifcx') {
|
|
184
|
+
// Export as IFCX JSON
|
|
185
|
+
const data = {
|
|
186
|
+
format: 'ifcx',
|
|
187
|
+
modelId: selectedModelId,
|
|
188
|
+
modelName: selectedModel.name,
|
|
189
|
+
schemaVersion: 'IFC5',
|
|
190
|
+
mutations: mutationView?.getMutations() || [],
|
|
191
|
+
exportedAt: new Date().toISOString(),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
195
|
+
const url = URL.createObjectURL(blob);
|
|
196
|
+
const a = document.createElement('a');
|
|
197
|
+
a.href = url;
|
|
198
|
+
a.download = `${selectedModel.name.replace(/\.[^.]+$/, '')}_modified.ifcx`;
|
|
199
|
+
document.body.appendChild(a);
|
|
200
|
+
a.click();
|
|
201
|
+
document.body.removeChild(a);
|
|
202
|
+
URL.revokeObjectURL(url);
|
|
203
|
+
|
|
204
|
+
setExportResult({
|
|
205
|
+
success: true,
|
|
206
|
+
message: `Exported IFCX with ${mutationView?.getMutations().length || 0} mutations`,
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
// Export mutations as JSON
|
|
210
|
+
const mutations = mutationView?.getMutations() || [];
|
|
211
|
+
const data = {
|
|
212
|
+
version: 1,
|
|
213
|
+
modelId: selectedModelId,
|
|
214
|
+
modelName: selectedModel.name,
|
|
215
|
+
mutations,
|
|
216
|
+
exportedAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
220
|
+
const url = URL.createObjectURL(blob);
|
|
221
|
+
const a = document.createElement('a');
|
|
222
|
+
a.href = url;
|
|
223
|
+
a.download = `${selectedModel.name.replace(/\.[^.]+$/, '')}_changes.json`;
|
|
224
|
+
document.body.appendChild(a);
|
|
225
|
+
a.click();
|
|
226
|
+
document.body.removeChild(a);
|
|
227
|
+
URL.revokeObjectURL(url);
|
|
228
|
+
|
|
229
|
+
setExportResult({
|
|
230
|
+
success: true,
|
|
231
|
+
message: `Exported ${mutations.length} changes as JSON`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Export failed:', error);
|
|
236
|
+
setExportResult({
|
|
237
|
+
success: false,
|
|
238
|
+
message: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
239
|
+
});
|
|
240
|
+
} finally {
|
|
241
|
+
setIsExporting(false);
|
|
242
|
+
}
|
|
243
|
+
}, [selectedModel, selectedModelId, format, schema, includeGeometry, applyMutations, deltaOnly, getMutationView, modifiedCount]);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
247
|
+
<DialogTrigger asChild>
|
|
248
|
+
{trigger || (
|
|
249
|
+
<Button variant="outline" size="sm">
|
|
250
|
+
<Download className="h-4 w-4 mr-2" />
|
|
251
|
+
Export IFC
|
|
252
|
+
</Button>
|
|
253
|
+
)}
|
|
254
|
+
</DialogTrigger>
|
|
255
|
+
<DialogContent className="sm:max-w-lg">
|
|
256
|
+
<DialogHeader>
|
|
257
|
+
<DialogTitle className="flex items-center gap-2">
|
|
258
|
+
<Download className="h-5 w-5" />
|
|
259
|
+
Export IFC File
|
|
260
|
+
</DialogTitle>
|
|
261
|
+
<DialogDescription>
|
|
262
|
+
Export your model with property modifications applied
|
|
263
|
+
</DialogDescription>
|
|
264
|
+
</DialogHeader>
|
|
265
|
+
|
|
266
|
+
<div className="grid gap-4 py-4">
|
|
267
|
+
{/* Model selector */}
|
|
268
|
+
<div className="flex items-center gap-4">
|
|
269
|
+
<Label className="w-32">Model</Label>
|
|
270
|
+
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
|
271
|
+
<SelectTrigger>
|
|
272
|
+
<SelectValue placeholder="Select model" />
|
|
273
|
+
</SelectTrigger>
|
|
274
|
+
<SelectContent>
|
|
275
|
+
{modelList.map((m) => (
|
|
276
|
+
<SelectItem key={m.id} value={m.id}>
|
|
277
|
+
<div className="flex items-center gap-2">
|
|
278
|
+
{m.name}
|
|
279
|
+
{m.isDirty && (
|
|
280
|
+
<Badge variant="secondary" className="text-xs">
|
|
281
|
+
modified
|
|
282
|
+
</Badge>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</SelectItem>
|
|
286
|
+
))}
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Format selector */}
|
|
292
|
+
<div className="flex items-center gap-4">
|
|
293
|
+
<Label className="w-32">Format</Label>
|
|
294
|
+
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}>
|
|
295
|
+
<SelectTrigger>
|
|
296
|
+
<SelectValue />
|
|
297
|
+
</SelectTrigger>
|
|
298
|
+
<SelectContent>
|
|
299
|
+
<SelectItem value="ifc">
|
|
300
|
+
<div className="flex items-center gap-2">
|
|
301
|
+
<FileText className="h-4 w-4" />
|
|
302
|
+
IFC (STEP)
|
|
303
|
+
</div>
|
|
304
|
+
</SelectItem>
|
|
305
|
+
<SelectItem value="ifcx">
|
|
306
|
+
<div className="flex items-center gap-2">
|
|
307
|
+
<FileJson className="h-4 w-4" />
|
|
308
|
+
IFCX (JSON)
|
|
309
|
+
</div>
|
|
310
|
+
</SelectItem>
|
|
311
|
+
<SelectItem value="json">
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<FileJson className="h-4 w-4" />
|
|
314
|
+
Changes Only (JSON)
|
|
315
|
+
</div>
|
|
316
|
+
</SelectItem>
|
|
317
|
+
</SelectContent>
|
|
318
|
+
</Select>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Schema version (for IFC format) */}
|
|
322
|
+
{format === 'ifc' && (
|
|
323
|
+
<div className="flex items-center gap-4">
|
|
324
|
+
<Label className="w-32">Schema</Label>
|
|
325
|
+
<Select value={schema} onValueChange={(v) => setSchema(v as SchemaVersion)}>
|
|
326
|
+
<SelectTrigger>
|
|
327
|
+
<SelectValue />
|
|
328
|
+
</SelectTrigger>
|
|
329
|
+
<SelectContent>
|
|
330
|
+
<SelectItem value="IFC2X3">IFC2X3</SelectItem>
|
|
331
|
+
<SelectItem value="IFC4">IFC4</SelectItem>
|
|
332
|
+
<SelectItem value="IFC4X3">IFC4X3</SelectItem>
|
|
333
|
+
</SelectContent>
|
|
334
|
+
</Select>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* Options */}
|
|
339
|
+
{format === 'ifc' && (
|
|
340
|
+
<>
|
|
341
|
+
<div className="flex items-center justify-between">
|
|
342
|
+
<Label>Include Geometry</Label>
|
|
343
|
+
<Switch checked={includeGeometry} onCheckedChange={setIncludeGeometry} />
|
|
344
|
+
</div>
|
|
345
|
+
<div className="flex items-center justify-between">
|
|
346
|
+
<Label>Apply Property Changes</Label>
|
|
347
|
+
<Switch checked={applyMutations} onCheckedChange={setApplyMutations} />
|
|
348
|
+
</div>
|
|
349
|
+
<div className="flex items-center justify-between">
|
|
350
|
+
<Label>Export Changes Only (Delta)</Label>
|
|
351
|
+
<Switch checked={deltaOnly} onCheckedChange={setDeltaOnly} />
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{/* Stats */}
|
|
357
|
+
{modifiedCount > 0 && (
|
|
358
|
+
<Alert>
|
|
359
|
+
<AlertCircle className="h-4 w-4" />
|
|
360
|
+
<AlertTitle>Pending Changes</AlertTitle>
|
|
361
|
+
<AlertDescription>
|
|
362
|
+
{modifiedCount} entities have been modified
|
|
363
|
+
</AlertDescription>
|
|
364
|
+
</Alert>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* Export result */}
|
|
368
|
+
{exportResult && (
|
|
369
|
+
<Alert variant={exportResult.success ? 'default' : 'destructive'}>
|
|
370
|
+
{exportResult.success ? (
|
|
371
|
+
<Check className="h-4 w-4" />
|
|
372
|
+
) : (
|
|
373
|
+
<AlertCircle className="h-4 w-4" />
|
|
374
|
+
)}
|
|
375
|
+
<AlertTitle>{exportResult.success ? 'Success' : 'Error'}</AlertTitle>
|
|
376
|
+
<AlertDescription>{exportResult.message}</AlertDescription>
|
|
377
|
+
</Alert>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<DialogFooter>
|
|
382
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
383
|
+
Cancel
|
|
384
|
+
</Button>
|
|
385
|
+
<Button onClick={handleExport} disabled={isExporting || !selectedModel}>
|
|
386
|
+
{isExporting ? (
|
|
387
|
+
<>
|
|
388
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
389
|
+
Exporting...
|
|
390
|
+
</>
|
|
391
|
+
) : (
|
|
392
|
+
<>
|
|
393
|
+
<Download className="h-4 w-4 mr-2" />
|
|
394
|
+
Export
|
|
395
|
+
</>
|
|
396
|
+
)}
|
|
397
|
+
</Button>
|
|
398
|
+
</DialogFooter>
|
|
399
|
+
</DialogContent>
|
|
400
|
+
</Dialog>
|
|
401
|
+
);
|
|
402
|
+
}
|