@ifc-lite/viewer 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -41,11 +41,12 @@ import {
41
41
  AlertTitle,
42
42
  } from '@/components/ui/alert';
43
43
  import { useViewerStore } from '@/store';
44
- import { StepExporter } from '@ifc-lite/export';
44
+ import { StepExporter, MergedExporter, type MergeModelInput } from '@ifc-lite/export';
45
45
  import { MutablePropertyView } from '@ifc-lite/mutations';
46
46
  import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
47
47
 
48
48
  type ExportFormat = 'ifc' | 'ifcx' | 'json';
49
+ type ExportScope = 'single' | 'merged';
49
50
  type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3';
50
51
 
51
52
  interface ExportDialogProps {
@@ -58,6 +59,10 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
58
59
  const getMutationView = useViewerStore((s) => s.getMutationView);
59
60
  const registerMutationView = useViewerStore((s) => s.registerMutationView);
60
61
  const getModifiedEntityCount = useViewerStore((s) => s.getModifiedEntityCount);
62
+ const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
63
+ const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
64
+ const hiddenEntitiesByModel = useViewerStore((s) => s.hiddenEntitiesByModel);
65
+ const isolatedEntitiesByModel = useViewerStore((s) => s.isolatedEntitiesByModel);
61
66
  // Also get legacy single-model state for backward compatibility
62
67
  const legacyIfcDataStore = useViewerStore((s) => s.ifcDataStore);
63
68
  const legacyGeometryResult = useViewerStore((s) => s.geometryResult);
@@ -66,9 +71,11 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
66
71
  const [format, setFormat] = useState<ExportFormat>('ifc');
67
72
  const [schema, setSchema] = useState<SchemaVersion>('IFC4');
68
73
  const [selectedModelId, setSelectedModelId] = useState<string>('');
74
+ const [exportScope, setExportScope] = useState<ExportScope>('single');
69
75
  const [includeGeometry, setIncludeGeometry] = useState(true);
70
76
  const [applyMutations, setApplyMutations] = useState(true);
71
77
  const [deltaOnly, setDeltaOnly] = useState(false);
78
+ const [visibleOnly, setVisibleOnly] = useState(false);
72
79
  const [isExporting, setIsExporting] = useState(false);
73
80
  const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null);
74
81
 
@@ -145,22 +152,137 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
145
152
  return getModifiedEntityCount();
146
153
  }, [getModifiedEntityCount]);
147
154
 
155
+ /**
156
+ * Convert global visibility state IDs to local expressIds for a given model.
157
+ * The store uses global IDs (localId + idOffset), but the exporter needs local IDs.
158
+ */
159
+ const getLocalHiddenIds = useCallback((modelId: string): Set<number> => {
160
+ // Legacy single-model path: no federation offset, global IDs = local IDs
161
+ if (modelId === '__legacy__') {
162
+ return hiddenEntities;
163
+ }
164
+
165
+ const model = models.get(modelId);
166
+ if (!model) return new Set();
167
+ const offset = model.idOffset ?? 0;
168
+
169
+ // Prefer per-model visibility state, fall back to legacy global state
170
+ const modelHidden = hiddenEntitiesByModel.get(modelId);
171
+ if (modelHidden && modelHidden.size > 0) {
172
+ return modelHidden; // Already local expressIds
173
+ }
174
+
175
+ // Federated model: convert global IDs to local
176
+ const localIds = new Set<number>();
177
+ for (const globalId of hiddenEntities) {
178
+ const localId = globalId - offset;
179
+ if (localId > 0 && localId <= model.maxExpressId) {
180
+ localIds.add(localId);
181
+ }
182
+ }
183
+ return localIds;
184
+ }, [models, hiddenEntities, hiddenEntitiesByModel]);
185
+
186
+ const getLocalIsolatedIds = useCallback((modelId: string): Set<number> | null => {
187
+ // Legacy single-model path: no federation offset, global IDs = local IDs
188
+ if (modelId === '__legacy__') {
189
+ return isolatedEntities;
190
+ }
191
+
192
+ const model = models.get(modelId);
193
+ if (!model) return null;
194
+ const offset = model.idOffset ?? 0;
195
+
196
+ // Prefer per-model isolation state
197
+ const modelIsolated = isolatedEntitiesByModel.get(modelId);
198
+ if (modelIsolated && modelIsolated.size > 0) {
199
+ return modelIsolated; // Already local expressIds
200
+ }
201
+
202
+ // Federated model: convert global IDs to local
203
+ if (!isolatedEntities) return null;
204
+ const localIds = new Set<number>();
205
+ for (const globalId of isolatedEntities) {
206
+ const localId = globalId - offset;
207
+ if (localId > 0 && localId <= model.maxExpressId) {
208
+ localIds.add(localId);
209
+ }
210
+ }
211
+ return localIds.size > 0 ? localIds : null;
212
+ }, [models, isolatedEntities, isolatedEntitiesByModel]);
213
+
148
214
  const handleExport = useCallback(async () => {
149
- if (!selectedModel) return;
215
+ if (exportScope === 'single' && !selectedModel) return;
150
216
 
151
217
  setIsExporting(true);
152
218
  setExportResult(null);
153
219
 
154
220
  try {
221
+ // Handle merged export of all models
222
+ if (format === 'ifc' && exportScope === 'merged') {
223
+ const mergeInputs: MergeModelInput[] = Array.from(models.values()).map((m) => ({
224
+ id: m.id,
225
+ name: m.name,
226
+ dataStore: m.ifcDataStore,
227
+ }));
228
+
229
+ const mergedExporter = new MergedExporter(mergeInputs);
230
+
231
+ // Build per-model visibility maps if visible-only export
232
+ const hiddenByModel = new Map<string, Set<number>>();
233
+ const isolatedByModel = new Map<string, Set<number> | null>();
234
+ if (visibleOnly) {
235
+ for (const m of models.values()) {
236
+ hiddenByModel.set(m.id, getLocalHiddenIds(m.id));
237
+ isolatedByModel.set(m.id, getLocalIsolatedIds(m.id));
238
+ }
239
+ }
240
+
241
+ const result = mergedExporter.export({
242
+ schema,
243
+ projectStrategy: 'keep-first',
244
+ visibleOnly,
245
+ hiddenEntityIdsByModel: hiddenByModel,
246
+ isolatedEntityIdsByModel: isolatedByModel,
247
+ description: `Merged export of ${mergeInputs.length} models from ifc-lite`,
248
+ application: 'ifc-lite',
249
+ });
250
+
251
+ const blob = new Blob([result.content], { type: 'text/plain' });
252
+ const url = URL.createObjectURL(blob);
253
+ const a = document.createElement('a');
254
+ a.href = url;
255
+ a.download = 'merged_export.ifc';
256
+ document.body.appendChild(a);
257
+ a.click();
258
+ document.body.removeChild(a);
259
+ URL.revokeObjectURL(url);
260
+
261
+ setExportResult({
262
+ success: true,
263
+ message: `Merged ${result.stats.modelCount} models, ${result.stats.totalEntityCount} entities`,
264
+ });
265
+ return;
266
+ }
267
+
268
+ if (!selectedModel) return;
155
269
  const mutationView = getMutationView(selectedModelId);
156
270
 
157
271
  if (format === 'ifc') {
158
272
  const exporter = new StepExporter(selectedModel.ifcDataStore, mutationView || undefined);
273
+
274
+ // Build visibility filter for visible-only export
275
+ const localHidden = visibleOnly ? getLocalHiddenIds(selectedModelId) : undefined;
276
+ const localIsolated = visibleOnly ? getLocalIsolatedIds(selectedModelId) : undefined;
277
+
159
278
  const result = exporter.export({
160
279
  schema,
161
280
  includeGeometry,
162
281
  applyMutations,
163
282
  deltaOnly,
283
+ visibleOnly,
284
+ hiddenEntityIds: localHidden,
285
+ isolatedEntityIds: localIsolated,
164
286
  description: `Exported from ifc-lite with ${modifiedCount} modifications`,
165
287
  application: 'ifc-lite',
166
288
  });
@@ -170,7 +292,8 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
170
292
  const url = URL.createObjectURL(blob);
171
293
  const a = document.createElement('a');
172
294
  a.href = url;
173
- a.download = `${selectedModel.name.replace(/\.[^.]+$/, '')}_modified.ifc`;
295
+ const suffix = visibleOnly ? '_visible' : '_modified';
296
+ a.download = `${selectedModel.name.replace(/\.[^.]+$/, '')}${suffix}.ifc`;
174
297
  document.body.appendChild(a);
175
298
  a.click();
176
299
  document.body.removeChild(a);
@@ -240,7 +363,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
240
363
  } finally {
241
364
  setIsExporting(false);
242
365
  }
243
- }, [selectedModel, selectedModelId, format, schema, includeGeometry, applyMutations, deltaOnly, getMutationView, modifiedCount]);
366
+ }, [selectedModel, selectedModelId, format, schema, exportScope, includeGeometry, applyMutations, deltaOnly, visibleOnly, getMutationView, getLocalHiddenIds, getLocalIsolatedIds, modifiedCount, models]);
244
367
 
245
368
  return (
246
369
  <Dialog open={open} onOpenChange={setOpen}>
@@ -252,7 +375,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
252
375
  </Button>
253
376
  )}
254
377
  </DialogTrigger>
255
- <DialogContent className="sm:max-w-lg">
378
+ <DialogContent className="sm:max-w-md overflow-hidden">
256
379
  <DialogHeader>
257
380
  <DialogTitle className="flex items-center gap-2">
258
381
  <Download className="h-5 w-5" />
@@ -264,7 +387,24 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
264
387
  </DialogHeader>
265
388
 
266
389
  <div className="grid gap-4 py-4">
267
- {/* Model selector */}
390
+ {/* Export scope selector (only when multiple models loaded) */}
391
+ {format === 'ifc' && modelList.length > 1 && (
392
+ <div className="flex items-center gap-4">
393
+ <Label className="w-32">Scope</Label>
394
+ <Select value={exportScope} onValueChange={(v) => setExportScope(v as ExportScope)}>
395
+ <SelectTrigger>
396
+ <SelectValue />
397
+ </SelectTrigger>
398
+ <SelectContent>
399
+ <SelectItem value="single">Single Model</SelectItem>
400
+ <SelectItem value="merged">Merged (All Models)</SelectItem>
401
+ </SelectContent>
402
+ </Select>
403
+ </div>
404
+ )}
405
+
406
+ {/* Model selector (only for single-model export) */}
407
+ {exportScope === 'single' && (
268
408
  <div className="flex items-center gap-4">
269
409
  <Label className="w-32">Model</Label>
270
410
  <Select value={selectedModelId} onValueChange={setSelectedModelId}>
@@ -272,21 +412,19 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
272
412
  <SelectValue placeholder="Select model" />
273
413
  </SelectTrigger>
274
414
  <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>
415
+ {modelList.map((m) => {
416
+ const maxLen = 24;
417
+ const displayName = m.name.length > maxLen ? m.name.slice(0, maxLen) + '\u2026' : m.name;
418
+ return (
419
+ <SelectItem key={m.id} value={m.id} title={m.name}>
420
+ {displayName}{m.isDirty ? ' *' : ''}
285
421
  </SelectItem>
286
- ))}
422
+ );
423
+ })}
287
424
  </SelectContent>
288
425
  </Select>
289
426
  </div>
427
+ )}
290
428
 
291
429
  {/* Format selector */}
292
430
  <div className="flex items-center gap-4">
@@ -338,6 +476,15 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
338
476
  {/* Options */}
339
477
  {format === 'ifc' && (
340
478
  <>
479
+ <div className="flex items-center justify-between">
480
+ <div>
481
+ <Label>Export Visible Only</Label>
482
+ <p className="text-xs text-muted-foreground">Only include entities currently visible in the 3D view</p>
483
+ </div>
484
+ <Switch checked={visibleOnly} onCheckedChange={setVisibleOnly} />
485
+ </div>
486
+ {exportScope === 'single' && (
487
+ <>
341
488
  <div className="flex items-center justify-between">
342
489
  <Label>Include Geometry</Label>
343
490
  <Switch checked={includeGeometry} onCheckedChange={setIncludeGeometry} />
@@ -350,6 +497,8 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
350
497
  <Label>Export Changes Only (Delta)</Label>
351
498
  <Switch checked={deltaOnly} onCheckedChange={setDeltaOnly} />
352
499
  </div>
500
+ </>
501
+ )}
353
502
  </>
354
503
  )}
355
504
 
@@ -15,7 +15,7 @@ import {
15
15
  import { Input } from '@/components/ui/input';
16
16
  import { Button } from '@/components/ui/button';
17
17
  import { cn } from '@/lib/utils';
18
- import { useViewerStore } from '@/store';
18
+ import { useViewerStore, resolveEntityRef } from '@/store';
19
19
  import { useIfc } from '@/hooks/useIfc';
20
20
 
21
21
  import type { TreeNode } from './hierarchy/types';
@@ -280,6 +280,8 @@ export function HierarchyPanel() {
280
280
  } else {
281
281
  // Legacy single-model: expressId = globalId (offset is 0)
282
282
  setSelectedEntityId(elementId);
283
+ // Also set selectedEntity for property panel (was missing, causing blank panel)
284
+ setSelectedEntity(resolveEntityRef(elementId));
283
285
  }
284
286
  }
285
287
  }, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements]);