@ifc-lite/viewer 1.11.4 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/dist/assets/{Arrow.dom-CJKOARso.js → Arrow.dom-VW5W1XFO.js} +1 -1
- package/dist/assets/{browser-BFafGolK.js → browser-C6mwD6n0.js} +1 -1
- package/dist/assets/{index-DFk6XTO_.js → index-BzoX4cQC.js} +17780 -16749
- package/dist/assets/index-Cx134arv.css +1 -0
- package/dist/assets/{index-D5uNqTWq.js → index-DQE23JyT.js} +4 -4
- package/dist/assets/{native-bridge-BBtlwehc.js → native-bridge-BibEEmFV.js} +1 -1
- package/dist/assets/{wasm-bridge-ZIqtzqnw.js → wasm-bridge-CYzUd3Io.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -19
- package/src/App.tsx +2 -0
- package/src/components/ui/toast.tsx +121 -0
- package/src/components/viewer/ExportChangesButton.tsx +3 -1
- package/src/components/viewer/ExportDialog.tsx +216 -131
- package/src/components/viewer/MainToolbar.tsx +9 -2
- package/src/components/viewer/PropertiesPanel.tsx +91 -0
- package/src/components/viewer/useGeometryStreaming.ts +4 -4
- package/src/sdk/adapters/lens-adapter.ts +1 -1
- package/src/sdk/adapters/viewer-adapter.ts +1 -1
- package/src/store/slices/measurementSlice.test.ts +22 -22
- package/src/store/slices/modelSlice.test.ts +2 -0
- package/dist/assets/index-BoYyWYAu.css +0 -1
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Export Dialog for IFC export with property mutations
|
|
7
|
+
*
|
|
8
|
+
* Schema drives the output format automatically:
|
|
9
|
+
* - IFC2X3 / IFC4 / IFC4X3 → .ifc (STEP)
|
|
10
|
+
* - IFC5 → .ifcx (JSON + USD geometry)
|
|
11
|
+
*
|
|
12
|
+
* "Changes Only" exports just mutations:
|
|
13
|
+
* - Below IFC5 → .json
|
|
14
|
+
* - IFC5 → .ifcx
|
|
7
15
|
*/
|
|
8
16
|
|
|
9
17
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
10
18
|
import {
|
|
11
19
|
Download,
|
|
12
|
-
FileText,
|
|
13
|
-
FileJson,
|
|
14
20
|
AlertCircle,
|
|
15
21
|
Check,
|
|
16
22
|
Loader2,
|
|
23
|
+
ArrowUp,
|
|
24
|
+
ArrowDown,
|
|
17
25
|
} from 'lucide-react';
|
|
18
26
|
import { Button } from '@/components/ui/button';
|
|
19
27
|
import { Label } from '@/components/ui/label';
|
|
@@ -41,13 +49,13 @@ import {
|
|
|
41
49
|
AlertTitle,
|
|
42
50
|
} from '@/components/ui/alert';
|
|
43
51
|
import { useViewerStore } from '@/store';
|
|
44
|
-
import {
|
|
52
|
+
import { toast } from '@/components/ui/toast';
|
|
53
|
+
import { StepExporter, MergedExporter, Ifc5Exporter, type MergeModelInput } from '@ifc-lite/export';
|
|
45
54
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
46
55
|
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
47
56
|
|
|
48
|
-
type ExportFormat = 'ifc' | 'ifcx' | 'json';
|
|
49
57
|
type ExportScope = 'single' | 'merged';
|
|
50
|
-
type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3';
|
|
58
|
+
type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
|
|
51
59
|
|
|
52
60
|
interface ExportDialogProps {
|
|
53
61
|
trigger?: React.ReactNode;
|
|
@@ -68,17 +76,19 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
68
76
|
const legacyGeometryResult = useViewerStore((s) => s.geometryResult);
|
|
69
77
|
|
|
70
78
|
const [open, setOpen] = useState(false);
|
|
71
|
-
const [
|
|
72
|
-
const [schema, setSchema] = useState<SchemaVersion>('IFC4');
|
|
79
|
+
const [schema, setSchema] = useState<SchemaVersion | ''>('');
|
|
73
80
|
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
|
74
81
|
const [exportScope, setExportScope] = useState<ExportScope>('single');
|
|
75
82
|
const [includeGeometry, setIncludeGeometry] = useState(true);
|
|
76
83
|
const [applyMutations, setApplyMutations] = useState(true);
|
|
77
|
-
const [
|
|
84
|
+
const [changesOnly, setChangesOnly] = useState(false);
|
|
78
85
|
const [visibleOnly, setVisibleOnly] = useState(false);
|
|
79
86
|
const [isExporting, setIsExporting] = useState(false);
|
|
80
87
|
const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
81
88
|
|
|
89
|
+
// Derived: is this an IFC5/IFCX export?
|
|
90
|
+
const isIfc5 = schema === 'IFC5';
|
|
91
|
+
|
|
82
92
|
// Get list of models with data stores - includes both federated models and legacy single-model
|
|
83
93
|
const modelList = useMemo(() => {
|
|
84
94
|
const list = Array.from(models.values()).map((m) => ({
|
|
@@ -148,6 +158,33 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
148
158
|
registerMutationView(selectedModelId, mutationView);
|
|
149
159
|
}, [selectedModel, selectedModelId, getMutationView, registerMutationView]);
|
|
150
160
|
|
|
161
|
+
// Default schema to selected model's schema version
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!selectedModel) return;
|
|
164
|
+
const modelSchema = selectedModel.schemaVersion as SchemaVersion;
|
|
165
|
+
if (modelSchema) {
|
|
166
|
+
setSchema(modelSchema);
|
|
167
|
+
}
|
|
168
|
+
}, [selectedModel?.schemaVersion]);
|
|
169
|
+
|
|
170
|
+
// Determine schema conversion direction
|
|
171
|
+
const sourceSchema = (selectedModel?.schemaVersion as SchemaVersion) || '';
|
|
172
|
+
const schemaConversion = useMemo(() => {
|
|
173
|
+
if (!sourceSchema || !schema) return null;
|
|
174
|
+
const order: Record<string, number> = { IFC2X3: 1, IFC4: 2, IFC4X3: 3, IFC5: 4 };
|
|
175
|
+
const src = order[sourceSchema] ?? 0;
|
|
176
|
+
const dst = order[schema] ?? 0;
|
|
177
|
+
if (src === dst) return null;
|
|
178
|
+
return src < dst ? 'upgrade' as const : 'downgrade' as const;
|
|
179
|
+
}, [sourceSchema, schema]);
|
|
180
|
+
|
|
181
|
+
// Reset scope to single when switching to IFC5 (merged not supported)
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (isIfc5) {
|
|
184
|
+
setExportScope('single');
|
|
185
|
+
}
|
|
186
|
+
}, [isIfc5]);
|
|
187
|
+
|
|
151
188
|
const modifiedCount = useMemo(() => {
|
|
152
189
|
return getModifiedEntityCount();
|
|
153
190
|
}, [getModifiedEntityCount]);
|
|
@@ -211,15 +248,28 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
211
248
|
return localIds.size > 0 ? localIds : null;
|
|
212
249
|
}, [models, isolatedEntities, isolatedEntitiesByModel]);
|
|
213
250
|
|
|
251
|
+
// Compute output format description for UI
|
|
252
|
+
const outputInfo = useMemo(() => {
|
|
253
|
+
if (changesOnly) {
|
|
254
|
+
return isIfc5
|
|
255
|
+
? { ext: '.ifcx', label: 'IFCX (JSON)' }
|
|
256
|
+
: { ext: '.json', label: 'JSON' };
|
|
257
|
+
}
|
|
258
|
+
return isIfc5
|
|
259
|
+
? { ext: '.ifcx', label: 'IFCX (JSON + USD geometry)' }
|
|
260
|
+
: { ext: '.ifc', label: 'IFC (STEP)' };
|
|
261
|
+
}, [isIfc5, changesOnly]);
|
|
262
|
+
|
|
214
263
|
const handleExport = useCallback(async () => {
|
|
264
|
+
if (!schema) return;
|
|
215
265
|
if (exportScope === 'single' && !selectedModel) return;
|
|
216
266
|
|
|
217
267
|
setIsExporting(true);
|
|
218
268
|
setExportResult(null);
|
|
219
269
|
|
|
220
270
|
try {
|
|
221
|
-
// Handle merged export of all models
|
|
222
|
-
if (
|
|
271
|
+
// Handle merged export of all models (STEP only, not IFC5)
|
|
272
|
+
if (!isIfc5 && exportScope === 'merged' && !changesOnly) {
|
|
223
273
|
const mergeInputs: MergeModelInput[] = Array.from(models.values()).map((m) => ({
|
|
224
274
|
id: m.id,
|
|
225
275
|
name: m.name,
|
|
@@ -258,59 +308,84 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
258
308
|
document.body.removeChild(a);
|
|
259
309
|
URL.revokeObjectURL(url);
|
|
260
310
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
311
|
+
const msg = `Merged ${result.stats.modelCount} models, ${result.stats.totalEntityCount} entities`;
|
|
312
|
+
setExportResult({ success: true, message: msg });
|
|
313
|
+
toast.success(msg);
|
|
265
314
|
return;
|
|
266
315
|
}
|
|
267
316
|
|
|
268
317
|
if (!selectedModel) return;
|
|
269
318
|
const mutationView = getMutationView(selectedModelId);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
319
|
+
const baseName = selectedModel.name.replace(/\.[^.]+$/, '');
|
|
320
|
+
|
|
321
|
+
// ── IFC5 → always IFCX ──────────────────────────────────────────
|
|
322
|
+
if (isIfc5) {
|
|
323
|
+
const federatedModel = models.get(selectedModelId);
|
|
324
|
+
const idOffset = federatedModel?.idOffset ?? 0;
|
|
325
|
+
|
|
326
|
+
const exporter = new Ifc5Exporter(
|
|
327
|
+
selectedModel.ifcDataStore,
|
|
328
|
+
selectedModel.geometryResult,
|
|
329
|
+
mutationView || undefined,
|
|
330
|
+
idOffset,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// When changesOnly, restrict to mutated entities and force applyMutations
|
|
334
|
+
let localHidden: Set<number> | undefined;
|
|
335
|
+
let localIsolated: Set<number> | undefined;
|
|
336
|
+
let effectiveVisibleOnly = visibleOnly;
|
|
337
|
+
let effectiveApplyMutations = applyMutations;
|
|
338
|
+
|
|
339
|
+
if (changesOnly && mutationView) {
|
|
340
|
+
// Compute the set of entity IDs that have mutations
|
|
341
|
+
const mutations = mutationView.getMutations();
|
|
342
|
+
const mutatedEntityIds = new Set<number>();
|
|
343
|
+
for (const m of mutations) {
|
|
344
|
+
mutatedEntityIds.add(m.entityId);
|
|
345
|
+
}
|
|
346
|
+
// Use isolatedEntityIds as an allowlist to export only mutated entities
|
|
347
|
+
localIsolated = mutatedEntityIds;
|
|
348
|
+
effectiveVisibleOnly = true;
|
|
349
|
+
effectiveApplyMutations = true;
|
|
350
|
+
} else if (visibleOnly) {
|
|
351
|
+
localHidden = getLocalHiddenIds(selectedModelId);
|
|
352
|
+
localIsolated = getLocalIsolatedIds(selectedModelId) ?? undefined;
|
|
353
|
+
effectiveVisibleOnly = true;
|
|
354
|
+
}
|
|
277
355
|
|
|
278
356
|
const result = exporter.export({
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
applyMutations,
|
|
282
|
-
|
|
283
|
-
visibleOnly,
|
|
357
|
+
includeGeometry: changesOnly ? false : includeGeometry,
|
|
358
|
+
includeProperties: true,
|
|
359
|
+
applyMutations: effectiveApplyMutations,
|
|
360
|
+
visibleOnly: effectiveVisibleOnly,
|
|
284
361
|
hiddenEntityIds: localHidden,
|
|
285
362
|
isolatedEntityIds: localIsolated,
|
|
286
|
-
|
|
287
|
-
application: 'ifc-lite',
|
|
363
|
+
author: 'ifc-lite',
|
|
288
364
|
});
|
|
289
365
|
|
|
290
|
-
|
|
291
|
-
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
366
|
+
const blob = new Blob([result.content], { type: 'application/json' });
|
|
292
367
|
const url = URL.createObjectURL(blob);
|
|
293
368
|
const a = document.createElement('a');
|
|
294
369
|
a.href = url;
|
|
295
|
-
const suffix = visibleOnly ? '_visible' : '
|
|
296
|
-
a.download = `${
|
|
370
|
+
const suffix = changesOnly ? '_changes' : (visibleOnly ? '_visible' : '_export');
|
|
371
|
+
a.download = `${baseName}${suffix}.ifcx`;
|
|
297
372
|
document.body.appendChild(a);
|
|
298
373
|
a.click();
|
|
299
374
|
document.body.removeChild(a);
|
|
300
375
|
URL.revokeObjectURL(url);
|
|
301
376
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
377
|
+
const ifcxMsg = `Exported IFCX: ${result.stats.nodeCount} nodes, ${result.stats.meshCount} meshes, ${result.stats.propertyCount} properties`;
|
|
378
|
+
setExportResult({ success: true, message: ifcxMsg });
|
|
379
|
+
toast.success(ifcxMsg);
|
|
380
|
+
|
|
381
|
+
// ── Changes only (pre-IFC5) → JSON ───────────────────────────────
|
|
382
|
+
} else if (changesOnly) {
|
|
383
|
+
const mutations = mutationView?.getMutations() || [];
|
|
308
384
|
const data = {
|
|
309
|
-
|
|
385
|
+
version: 1,
|
|
310
386
|
modelId: selectedModelId,
|
|
311
387
|
modelName: selectedModel.name,
|
|
312
|
-
|
|
313
|
-
mutations: mutationView?.getMutations() || [],
|
|
388
|
+
mutations,
|
|
314
389
|
exportedAt: new Date().toISOString(),
|
|
315
390
|
};
|
|
316
391
|
|
|
@@ -318,52 +393,58 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
318
393
|
const url = URL.createObjectURL(blob);
|
|
319
394
|
const a = document.createElement('a');
|
|
320
395
|
a.href = url;
|
|
321
|
-
a.download = `${
|
|
396
|
+
a.download = `${baseName}_changes.json`;
|
|
322
397
|
document.body.appendChild(a);
|
|
323
398
|
a.click();
|
|
324
399
|
document.body.removeChild(a);
|
|
325
400
|
URL.revokeObjectURL(url);
|
|
326
401
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
402
|
+
const jsonMsg = `Exported ${mutations.length} changes as JSON`;
|
|
403
|
+
setExportResult({ success: true, message: jsonMsg });
|
|
404
|
+
toast.success(jsonMsg);
|
|
405
|
+
|
|
406
|
+
// ── Pre-IFC5 full export → STEP ──────────────────────────────────
|
|
331
407
|
} else {
|
|
332
|
-
|
|
333
|
-
const mutations = mutationView?.getMutations() || [];
|
|
334
|
-
const data = {
|
|
335
|
-
version: 1,
|
|
336
|
-
modelId: selectedModelId,
|
|
337
|
-
modelName: selectedModel.name,
|
|
338
|
-
mutations,
|
|
339
|
-
exportedAt: new Date().toISOString(),
|
|
340
|
-
};
|
|
408
|
+
const exporter = new StepExporter(selectedModel.ifcDataStore, mutationView || undefined);
|
|
341
409
|
|
|
342
|
-
const
|
|
410
|
+
const localHidden = visibleOnly ? getLocalHiddenIds(selectedModelId) : undefined;
|
|
411
|
+
const localIsolated = visibleOnly ? getLocalIsolatedIds(selectedModelId) : undefined;
|
|
412
|
+
|
|
413
|
+
const result = exporter.export({
|
|
414
|
+
schema,
|
|
415
|
+
includeGeometry,
|
|
416
|
+
applyMutations,
|
|
417
|
+
visibleOnly,
|
|
418
|
+
hiddenEntityIds: localHidden,
|
|
419
|
+
isolatedEntityIds: localIsolated,
|
|
420
|
+
description: `Exported from ifc-lite with ${modifiedCount} modifications`,
|
|
421
|
+
application: 'ifc-lite',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
343
425
|
const url = URL.createObjectURL(blob);
|
|
344
426
|
const a = document.createElement('a');
|
|
345
427
|
a.href = url;
|
|
346
|
-
|
|
428
|
+
const suffix = visibleOnly ? '_visible' : '_export';
|
|
429
|
+
a.download = `${baseName}${suffix}.ifc`;
|
|
347
430
|
document.body.appendChild(a);
|
|
348
431
|
a.click();
|
|
349
432
|
document.body.removeChild(a);
|
|
350
433
|
URL.revokeObjectURL(url);
|
|
351
434
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
435
|
+
const stepMsg = `Exported ${result.stats.entityCount} entities (${result.stats.modifiedEntityCount} modified)`;
|
|
436
|
+
setExportResult({ success: true, message: stepMsg });
|
|
437
|
+
toast.success(stepMsg);
|
|
356
438
|
}
|
|
357
439
|
} catch (error) {
|
|
358
440
|
console.error('Export failed:', error);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
441
|
+
const errMsg = `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
442
|
+
setExportResult({ success: false, message: errMsg });
|
|
443
|
+
toast.error(errMsg);
|
|
363
444
|
} finally {
|
|
364
445
|
setIsExporting(false);
|
|
365
446
|
}
|
|
366
|
-
}, [selectedModel, selectedModelId,
|
|
447
|
+
}, [selectedModel, selectedModelId, schema, isIfc5, exportScope, includeGeometry, applyMutations, changesOnly, visibleOnly, getMutationView, getLocalHiddenIds, getLocalIsolatedIds, modifiedCount, models]);
|
|
367
448
|
|
|
368
449
|
return (
|
|
369
450
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
@@ -387,8 +468,8 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
387
468
|
</DialogHeader>
|
|
388
469
|
|
|
389
470
|
<div className="grid gap-4 py-4">
|
|
390
|
-
{/*
|
|
391
|
-
{
|
|
471
|
+
{/* Scope selector (only for STEP schemas with multiple models) */}
|
|
472
|
+
{!isIfc5 && !changesOnly && modelList.length > 1 && (
|
|
392
473
|
<div className="flex items-center gap-4">
|
|
393
474
|
<Label className="w-32">Scope</Label>
|
|
394
475
|
<Select value={exportScope} onValueChange={(v) => setExportScope(v as ExportScope)}>
|
|
@@ -417,7 +498,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
417
498
|
const displayName = m.name.length > maxLen ? m.name.slice(0, maxLen) + '\u2026' : m.name;
|
|
418
499
|
return (
|
|
419
500
|
<SelectItem key={m.id} value={m.id} title={m.name}>
|
|
420
|
-
{displayName}{m.isDirty ? ' *' : ''}
|
|
501
|
+
{displayName}{m.isDirty ? ' *' : ''}{m.schemaVersion ? ` (${m.schemaVersion})` : ''}
|
|
421
502
|
</SelectItem>
|
|
422
503
|
);
|
|
423
504
|
})}
|
|
@@ -426,80 +507,84 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
426
507
|
</div>
|
|
427
508
|
)}
|
|
428
509
|
|
|
429
|
-
{/*
|
|
510
|
+
{/* Schema selector — this drives the output format */}
|
|
430
511
|
<div className="flex items-center gap-4">
|
|
431
|
-
<Label className="w-32">
|
|
432
|
-
<Select value={
|
|
512
|
+
<Label className="w-32">Schema</Label>
|
|
513
|
+
<Select value={schema} onValueChange={(v) => setSchema(v as SchemaVersion)}>
|
|
433
514
|
<SelectTrigger>
|
|
434
515
|
<SelectValue />
|
|
435
516
|
</SelectTrigger>
|
|
436
517
|
<SelectContent>
|
|
437
|
-
|
|
438
|
-
<
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
</
|
|
442
|
-
|
|
443
|
-
<SelectItem value="ifcx">
|
|
444
|
-
<div className="flex items-center gap-2">
|
|
445
|
-
<FileJson className="h-4 w-4" />
|
|
446
|
-
IFCX (JSON)
|
|
447
|
-
</div>
|
|
448
|
-
</SelectItem>
|
|
449
|
-
<SelectItem value="json">
|
|
450
|
-
<div className="flex items-center gap-2">
|
|
451
|
-
<FileJson className="h-4 w-4" />
|
|
452
|
-
Changes Only (JSON)
|
|
453
|
-
</div>
|
|
454
|
-
</SelectItem>
|
|
518
|
+
{(['IFC2X3', 'IFC4', 'IFC4X3', 'IFC5'] as const).map((v) => (
|
|
519
|
+
<SelectItem key={v} value={v}>
|
|
520
|
+
{v === 'IFC5' ? 'IFC5 (Alpha)' : v}
|
|
521
|
+
{v === sourceSchema ? ' (current)' : ''}
|
|
522
|
+
</SelectItem>
|
|
523
|
+
))}
|
|
455
524
|
</SelectContent>
|
|
456
525
|
</Select>
|
|
457
526
|
</div>
|
|
458
527
|
|
|
459
|
-
{/* Schema
|
|
460
|
-
{
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
528
|
+
{/* Schema conversion warning */}
|
|
529
|
+
{schemaConversion && (
|
|
530
|
+
<Alert variant={schemaConversion === 'downgrade' ? 'destructive' : 'default'}>
|
|
531
|
+
{schemaConversion === 'upgrade' ? (
|
|
532
|
+
<ArrowUp className="h-4 w-4" />
|
|
533
|
+
) : (
|
|
534
|
+
<ArrowDown className="h-4 w-4" />
|
|
535
|
+
)}
|
|
536
|
+
<AlertTitle>
|
|
537
|
+
Schema {schemaConversion === 'upgrade' ? 'Upgrade' : 'Downgrade'}
|
|
538
|
+
</AlertTitle>
|
|
539
|
+
<AlertDescription>
|
|
540
|
+
Converting from {sourceSchema} to {schema}.
|
|
541
|
+
{schemaConversion === 'downgrade'
|
|
542
|
+
? ' Some data may be lost in the conversion to an older schema.'
|
|
543
|
+
: ' Entity types will be mapped to the newer schema.'}
|
|
544
|
+
</AlertDescription>
|
|
545
|
+
</Alert>
|
|
474
546
|
)}
|
|
475
547
|
|
|
548
|
+
{/* Output format indicator */}
|
|
549
|
+
<div className="flex items-center gap-4">
|
|
550
|
+
<Label className="w-32 text-muted-foreground">Output</Label>
|
|
551
|
+
<Badge variant="secondary">{outputInfo.label}</Badge>
|
|
552
|
+
<span className="text-xs text-muted-foreground">{outputInfo.ext}</span>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
476
555
|
{/* Options */}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
556
|
+
<div className="flex items-center justify-between">
|
|
557
|
+
<div>
|
|
558
|
+
<Label>Export Visible Only</Label>
|
|
559
|
+
<p className="text-xs text-muted-foreground">Only include entities currently visible in the 3D view</p>
|
|
560
|
+
</div>
|
|
561
|
+
<Switch checked={visibleOnly} onCheckedChange={setVisibleOnly} />
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
{!changesOnly && exportScope === 'single' && (
|
|
565
|
+
<div className="flex items-center justify-between">
|
|
566
|
+
<Label>Include Geometry</Label>
|
|
567
|
+
<Switch checked={includeGeometry} onCheckedChange={setIncludeGeometry} />
|
|
568
|
+
</div>
|
|
569
|
+
)}
|
|
570
|
+
|
|
571
|
+
{exportScope === 'single' && (
|
|
572
|
+
<div className="flex items-center justify-between">
|
|
573
|
+
<Label>Apply Property Changes</Label>
|
|
574
|
+
<Switch checked={applyMutations} onCheckedChange={setApplyMutations} />
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
|
|
578
|
+
{exportScope === 'single' && (
|
|
579
|
+
<div className="flex items-center justify-between">
|
|
580
|
+
<div>
|
|
581
|
+
<Label>Changes Only</Label>
|
|
582
|
+
<p className="text-xs text-muted-foreground">
|
|
583
|
+
{isIfc5 ? 'Export as IFCX overlay with mutations only' : 'Export mutations as JSON delta'}
|
|
584
|
+
</p>
|
|
499
585
|
</div>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
</>
|
|
586
|
+
<Switch checked={changesOnly} onCheckedChange={setChangesOnly} />
|
|
587
|
+
</div>
|
|
503
588
|
)}
|
|
504
589
|
|
|
505
590
|
{/* Stats */}
|
|
@@ -531,7 +616,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
531
616
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
532
617
|
Cancel
|
|
533
618
|
</Button>
|
|
534
|
-
<Button onClick={handleExport} disabled={isExporting || !selectedModel}>
|
|
619
|
+
<Button onClick={handleExport} disabled={isExporting || !selectedModel || !schema}>
|
|
535
620
|
{isExporting ? (
|
|
536
621
|
<>
|
|
537
622
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
@@ -67,6 +67,7 @@ import { ExportChangesButton } from './ExportChangesButton';
|
|
|
67
67
|
import { useFloorplanView } from '@/hooks/useFloorplanView';
|
|
68
68
|
import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
|
|
69
69
|
import { ThemeSwitch } from './ThemeSwitch';
|
|
70
|
+
import { toast } from '@/components/ui/toast';
|
|
70
71
|
|
|
71
72
|
type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
|
|
72
73
|
|
|
@@ -332,7 +333,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
332
333
|
try {
|
|
333
334
|
const exporter = new GLTFExporter(geometryResult);
|
|
334
335
|
const glb = exporter.exportGLB({ includeMetadata: true });
|
|
335
|
-
// Create a new Uint8Array from the buffer to ensure correct typing
|
|
336
336
|
const blob = new Blob([new Uint8Array(glb)], { type: 'model/gltf-binary' });
|
|
337
337
|
const url = URL.createObjectURL(blob);
|
|
338
338
|
const a = document.createElement('a');
|
|
@@ -340,8 +340,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
340
340
|
a.download = 'model.glb';
|
|
341
341
|
a.click();
|
|
342
342
|
URL.revokeObjectURL(url);
|
|
343
|
+
toast.success(`Exported GLB (${(blob.size / 1024).toFixed(0)} KB)`);
|
|
343
344
|
} catch (err) {
|
|
344
345
|
console.error('Export failed:', err);
|
|
346
|
+
toast.error(`GLB export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
345
347
|
}
|
|
346
348
|
}, [geometryResult]);
|
|
347
349
|
|
|
@@ -354,8 +356,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
354
356
|
a.href = dataUrl;
|
|
355
357
|
a.download = 'screenshot.png';
|
|
356
358
|
a.click();
|
|
359
|
+
toast.success('Screenshot saved');
|
|
357
360
|
} catch (err) {
|
|
358
361
|
console.error('Screenshot failed:', err);
|
|
362
|
+
toast.error('Screenshot failed');
|
|
359
363
|
}
|
|
360
364
|
}, []);
|
|
361
365
|
|
|
@@ -392,15 +396,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
392
396
|
a.download = filename;
|
|
393
397
|
a.click();
|
|
394
398
|
URL.revokeObjectURL(url);
|
|
399
|
+
toast.success(`Exported ${type} CSV`);
|
|
395
400
|
} catch (err) {
|
|
396
401
|
console.error('CSV export failed:', err);
|
|
402
|
+
toast.error(`CSV export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
397
403
|
}
|
|
398
404
|
}, [ifcDataStore]);
|
|
399
405
|
|
|
400
406
|
const handleExportJSON = useCallback(() => {
|
|
401
407
|
if (!ifcDataStore) return;
|
|
402
408
|
try {
|
|
403
|
-
// Export basic JSON structure of entities
|
|
404
409
|
const entities: Record<string, unknown>[] = [];
|
|
405
410
|
for (let i = 0; i < ifcDataStore.entities.count; i++) {
|
|
406
411
|
const id = ifcDataStore.entities.expressId[i];
|
|
@@ -421,8 +426,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
421
426
|
a.download = 'model-data.json';
|
|
422
427
|
a.click();
|
|
423
428
|
URL.revokeObjectURL(url);
|
|
429
|
+
toast.success(`Exported ${entities.length} entities as JSON`);
|
|
424
430
|
} catch (err) {
|
|
425
431
|
console.error('JSON export failed:', err);
|
|
432
|
+
toast.error(`JSON export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
426
433
|
}
|
|
427
434
|
}, [ifcDataStore]);
|
|
428
435
|
|