@ifc-lite/viewer 1.6.0 → 1.7.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 +78 -0
  2. package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
  3. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  4. package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
  5. package/dist/assets/index-yTqs8kgX.css +1 -0
  6. package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
  7. package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/package.json +18 -15
  10. package/src/components/viewer/BCFPanel.tsx +7 -789
  11. package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
  12. package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
  13. package/src/components/viewer/HierarchyPanel.tsx +110 -842
  14. package/src/components/viewer/IDSExportDialog.tsx +281 -0
  15. package/src/components/viewer/IDSPanel.tsx +126 -17
  16. package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
  17. package/src/components/viewer/LensPanel.tsx +603 -0
  18. package/src/components/viewer/MainToolbar.tsx +188 -21
  19. package/src/components/viewer/PropertiesPanel.tsx +171 -663
  20. package/src/components/viewer/PropertyEditor.tsx +866 -77
  21. package/src/components/viewer/Section2DPanel.tsx +76 -2648
  22. package/src/components/viewer/ToolOverlays.tsx +3 -1097
  23. package/src/components/viewer/ViewerLayout.tsx +132 -45
  24. package/src/components/viewer/Viewport.tsx +237 -1659
  25. package/src/components/viewer/ViewportContainer.tsx +11 -3
  26. package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
  27. package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
  28. package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
  29. package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
  30. package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
  31. package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
  32. package/src/components/viewer/hierarchy/types.ts +54 -0
  33. package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
  34. package/src/components/viewer/lists/ListBuilder.tsx +486 -0
  35. package/src/components/viewer/lists/ListPanel.tsx +540 -0
  36. package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
  37. package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
  38. package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
  39. package/src/components/viewer/properties/DocumentCard.tsx +89 -0
  40. package/src/components/viewer/properties/MaterialCard.tsx +201 -0
  41. package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
  42. package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
  43. package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
  44. package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
  45. package/src/components/viewer/properties/encodingUtils.ts +29 -0
  46. package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
  47. package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
  48. package/src/components/viewer/tools/SectionPanel.tsx +183 -0
  49. package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
  50. package/src/components/viewer/tools/formatDistance.ts +18 -0
  51. package/src/components/viewer/tools/sectionConstants.ts +14 -0
  52. package/src/components/viewer/useAnimationLoop.ts +166 -0
  53. package/src/components/viewer/useGeometryStreaming.ts +398 -0
  54. package/src/components/viewer/useKeyboardControls.ts +221 -0
  55. package/src/components/viewer/useMouseControls.ts +1009 -0
  56. package/src/components/viewer/useRenderUpdates.ts +165 -0
  57. package/src/components/viewer/useTouchControls.ts +245 -0
  58. package/src/hooks/ids/idsColorSystem.ts +125 -0
  59. package/src/hooks/ids/idsDataAccessor.ts +237 -0
  60. package/src/hooks/ids/idsExportService.ts +444 -0
  61. package/src/hooks/useBCF.ts +7 -0
  62. package/src/hooks/useDrawingExport.ts +627 -0
  63. package/src/hooks/useDrawingGeneration.ts +627 -0
  64. package/src/hooks/useFloorplanView.ts +108 -0
  65. package/src/hooks/useIDS.ts +270 -463
  66. package/src/hooks/useIfc.ts +26 -1628
  67. package/src/hooks/useIfcFederation.ts +803 -0
  68. package/src/hooks/useIfcLoader.ts +508 -0
  69. package/src/hooks/useIfcServer.ts +465 -0
  70. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  71. package/src/hooks/useLens.ts +129 -0
  72. package/src/hooks/useMeasure2D.ts +365 -0
  73. package/src/hooks/useViewControls.ts +218 -0
  74. package/src/lib/ifc4-pset-definitions.test.ts +161 -0
  75. package/src/lib/ifc4-pset-definitions.ts +621 -0
  76. package/src/lib/ifc4-qto-definitions.ts +315 -0
  77. package/src/lib/lens/adapter.ts +138 -0
  78. package/src/lib/lens/index.ts +5 -0
  79. package/src/lib/lists/adapter.ts +69 -0
  80. package/src/lib/lists/index.ts +28 -0
  81. package/src/lib/lists/persistence.ts +64 -0
  82. package/src/services/fs-cache.ts +1 -1
  83. package/src/services/tauri-modules.d.ts +25 -0
  84. package/src/store/index.ts +38 -2
  85. package/src/store/slices/cameraSlice.ts +14 -1
  86. package/src/store/slices/dataSlice.ts +14 -1
  87. package/src/store/slices/lensSlice.ts +184 -0
  88. package/src/store/slices/listSlice.ts +74 -0
  89. package/src/store/slices/pinboardSlice.ts +114 -0
  90. package/src/store/types.ts +5 -0
  91. package/src/utils/ifcConfig.ts +16 -3
  92. package/src/utils/serverDataModel.ts +64 -101
  93. package/src/vite-env.d.ts +3 -0
  94. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  95. package/dist/assets/index-v3mcCUPN.css +0 -1
@@ -0,0 +1,335 @@
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
+ * Model metadata panel - displays file info, schema version, entity counts,
7
+ * coordinate system info, and project information.
8
+ */
9
+
10
+ import { useMemo } from 'react';
11
+ import {
12
+ Layers,
13
+ FileText,
14
+ Tag,
15
+ FileBox,
16
+ Clock,
17
+ HardDrive,
18
+ Hash,
19
+ Database,
20
+ Building2,
21
+ Globe,
22
+ Ruler,
23
+ } from 'lucide-react';
24
+ import { ScrollArea } from '@/components/ui/scroll-area';
25
+ import { PropertySetCard } from './PropertySetCard';
26
+ import type { PropertySet } from './encodingUtils';
27
+ import type { FederatedModel } from '@/store/types';
28
+ import { extractGeoreferencingOnDemand, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
29
+
30
+ /** Model metadata panel - displays file info, schema version, entity counts, etc. */
31
+ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
32
+ const dataStore = model.ifcDataStore;
33
+
34
+ // Format file size
35
+ const formatFileSize = (bytes: number): string => {
36
+ if (bytes < 1024) return `${bytes} B`;
37
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
38
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
39
+ };
40
+
41
+ // Format date
42
+ const formatDate = (timestamp: number): string => {
43
+ return new Date(timestamp).toLocaleString();
44
+ };
45
+
46
+ // Get IfcProject data if available
47
+ const projectData = useMemo(() => {
48
+ if (!dataStore?.spatialHierarchy?.project) return null;
49
+ const project = dataStore.spatialHierarchy.project;
50
+ const projectId = project.expressId;
51
+
52
+ // Get project entity attributes
53
+ const name = dataStore.entities.getName(projectId);
54
+ const globalId = dataStore.entities.getGlobalId(projectId);
55
+ const description = dataStore.entities.getDescription(projectId);
56
+
57
+ // Get project properties
58
+ const properties: PropertySet[] = [];
59
+ if (dataStore.properties) {
60
+ for (const pset of dataStore.properties.getForEntity(projectId)) {
61
+ properties.push({
62
+ name: pset.name,
63
+ properties: pset.properties.map(p => ({ name: p.name, value: p.value })),
64
+ });
65
+ }
66
+ }
67
+
68
+ return { name, globalId, description, properties };
69
+ }, [dataStore]);
70
+
71
+ // Count storeys and elements
72
+ const stats = useMemo(() => {
73
+ if (!dataStore?.spatialHierarchy) {
74
+ return { storeys: 0, elementsWithGeometry: 0 };
75
+ }
76
+ const storeys = dataStore.spatialHierarchy.byStorey.size;
77
+ let elementsWithGeometry = 0;
78
+ for (const elements of dataStore.spatialHierarchy.byStorey.values()) {
79
+ elementsWithGeometry += (elements as number[]).length;
80
+ }
81
+ return { storeys, elementsWithGeometry };
82
+ }, [dataStore]);
83
+
84
+ // Extract georeferencing info
85
+ const georef = useMemo(() => {
86
+ if (!dataStore) return null;
87
+ const info = extractGeoreferencingOnDemand(dataStore as IfcDataStore);
88
+ return info?.hasGeoreference ? info : null;
89
+ }, [dataStore]);
90
+
91
+ // Extract length unit scale
92
+ const unitInfo = useMemo(() => {
93
+ if (!dataStore?.source?.length || !dataStore?.entityIndex) return null;
94
+ const scale = extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
95
+ let unitName = 'Meters';
96
+ if (Math.abs(scale - 0.001) < 0.0001) unitName = 'Millimeters';
97
+ else if (Math.abs(scale - 0.01) < 0.001) unitName = 'Centimeters';
98
+ else if (Math.abs(scale - 0.0254) < 0.001) unitName = 'Inches';
99
+ else if (Math.abs(scale - 0.3048) < 0.01) unitName = 'Feet';
100
+ return { scale, unitName };
101
+ }, [dataStore]);
102
+
103
+ return (
104
+ <div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
105
+ {/* Header */}
106
+ <div className="p-4 border-b-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black space-y-3">
107
+ <div className="flex items-start gap-3">
108
+ <div className="p-2 border-2 border-primary/30 bg-primary/10 shrink-0 shadow-[2px_2px_0px_0px_rgba(0,0,0,0.1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.1)]">
109
+ <FileBox className="h-5 w-5 text-primary" />
110
+ </div>
111
+ <div className="flex-1 min-w-0 pt-0.5">
112
+ <h3 className="font-bold text-sm truncate uppercase tracking-tight text-zinc-900 dark:text-zinc-100">
113
+ {model.name}
114
+ </h3>
115
+ <p className="text-xs font-mono text-zinc-500 dark:text-zinc-400">IFC Model</p>
116
+ </div>
117
+ </div>
118
+
119
+ {/* Schema badge */}
120
+ <div className="flex items-center gap-2">
121
+ <span className="text-[10px] font-mono bg-primary/10 border border-primary/30 px-2 py-1 text-primary font-bold uppercase">
122
+ {model.schemaVersion}
123
+ </span>
124
+ </div>
125
+ </div>
126
+
127
+ <ScrollArea className="flex-1">
128
+ {/* File Information */}
129
+ <div className="border-b border-zinc-200 dark:border-zinc-800">
130
+ <div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
131
+ <h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
132
+ File Information
133
+ </h4>
134
+ </div>
135
+ <div className="divide-y divide-zinc-100 dark:divide-zinc-900">
136
+ <div className="flex items-center gap-3 px-3 py-2">
137
+ <HardDrive className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
138
+ <span className="text-xs text-zinc-500">File Size</span>
139
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
140
+ {formatFileSize(model.fileSize)}
141
+ </span>
142
+ </div>
143
+ <div className="flex items-center gap-3 px-3 py-2">
144
+ <Clock className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
145
+ <span className="text-xs text-zinc-500">Loaded At</span>
146
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
147
+ {formatDate(model.loadedAt)}
148
+ </span>
149
+ </div>
150
+ {dataStore && (
151
+ <div className="flex items-center gap-3 px-3 py-2">
152
+ <Clock className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
153
+ <span className="text-xs text-zinc-500">Parse Time</span>
154
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
155
+ {dataStore.parseTime.toFixed(0)} ms
156
+ </span>
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+
162
+ {/* Length Unit */}
163
+ {unitInfo && (
164
+ <div className="border-b border-zinc-200 dark:border-zinc-800">
165
+ <div className="flex items-center gap-3 px-3 py-2.5 bg-amber-50/50 dark:bg-amber-950/20">
166
+ <Ruler className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 shrink-0" />
167
+ <span className="text-xs font-bold text-amber-700 dark:text-amber-400 uppercase tracking-wide">Length Unit</span>
168
+ <span className="text-xs font-mono text-amber-800 dark:text-amber-300 ml-auto">
169
+ {unitInfo.unitName} ({unitInfo.scale})
170
+ </span>
171
+ </div>
172
+ </div>
173
+ )}
174
+
175
+ {/* Entity Statistics */}
176
+ <div className="border-b border-zinc-200 dark:border-zinc-800">
177
+ <div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
178
+ <h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
179
+ Statistics
180
+ </h4>
181
+ </div>
182
+ <div className="divide-y divide-zinc-100 dark:divide-zinc-900">
183
+ <div className="flex items-center gap-3 px-3 py-2">
184
+ <Database className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
185
+ <span className="text-xs text-zinc-500">Total Entities</span>
186
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
187
+ {dataStore?.entityCount?.toLocaleString() ?? 'N/A'}
188
+ </span>
189
+ </div>
190
+ <div className="flex items-center gap-3 px-3 py-2">
191
+ <Layers className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
192
+ <span className="text-xs text-zinc-500">Building Storeys</span>
193
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
194
+ {stats.storeys}
195
+ </span>
196
+ </div>
197
+ <div className="flex items-center gap-3 px-3 py-2">
198
+ <Building2 className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
199
+ <span className="text-xs text-zinc-500">Elements with Geometry</span>
200
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
201
+ {stats.elementsWithGeometry.toLocaleString()}
202
+ </span>
203
+ </div>
204
+ <div className="flex items-center gap-3 px-3 py-2">
205
+ <Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
206
+ <span className="text-xs text-zinc-500">Max Express ID</span>
207
+ <span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
208
+ {model.maxExpressId.toLocaleString()}
209
+ </span>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ {/* Georeferencing */}
215
+ {georef && (
216
+ <div className="border-b border-zinc-200 dark:border-zinc-800">
217
+ <div className="p-3 bg-teal-50/50 dark:bg-teal-950/20">
218
+ <div className="flex items-center gap-2">
219
+ <Globe className="h-3.5 w-3.5 text-teal-600 dark:text-teal-400" />
220
+ <h4 className="font-bold text-xs uppercase tracking-wide text-teal-700 dark:text-teal-300">
221
+ Georeferencing
222
+ </h4>
223
+ </div>
224
+ </div>
225
+ <div className="divide-y divide-zinc-100 dark:divide-zinc-900">
226
+ {georef.projectedCRS?.name && (
227
+ <div className="flex items-center gap-3 px-3 py-2">
228
+ <span className="text-xs text-zinc-500 shrink-0">CRS</span>
229
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto truncate max-w-[65%]">
230
+ {georef.projectedCRS.name}
231
+ </span>
232
+ </div>
233
+ )}
234
+ {georef.projectedCRS?.geodeticDatum && (
235
+ <div className="flex items-center gap-3 px-3 py-2">
236
+ <span className="text-xs text-zinc-500 shrink-0">Geodetic Datum</span>
237
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto">
238
+ {georef.projectedCRS.geodeticDatum}
239
+ </span>
240
+ </div>
241
+ )}
242
+ {georef.projectedCRS?.mapProjection && (
243
+ <div className="flex items-center gap-3 px-3 py-2">
244
+ <span className="text-xs text-zinc-500 shrink-0">Projection</span>
245
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto truncate max-w-[65%]">
246
+ {georef.projectedCRS.mapProjection}
247
+ </span>
248
+ </div>
249
+ )}
250
+ {georef.mapConversion && (
251
+ <>
252
+ <div className="flex items-center gap-3 px-3 py-2">
253
+ <span className="text-xs text-zinc-500 shrink-0">Eastings</span>
254
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
255
+ {georef.mapConversion.eastings.toFixed(3)}
256
+ </span>
257
+ </div>
258
+ <div className="flex items-center gap-3 px-3 py-2">
259
+ <span className="text-xs text-zinc-500 shrink-0">Northings</span>
260
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
261
+ {georef.mapConversion.northings.toFixed(3)}
262
+ </span>
263
+ </div>
264
+ <div className="flex items-center gap-3 px-3 py-2">
265
+ <span className="text-xs text-zinc-500 shrink-0">Height</span>
266
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto tabular-nums">
267
+ {georef.mapConversion.orthogonalHeight.toFixed(3)}
268
+ </span>
269
+ </div>
270
+ {georef.mapConversion.scale != null && georef.mapConversion.scale !== 1.0 && (
271
+ <div className="flex items-center gap-3 px-3 py-2">
272
+ <span className="text-xs text-zinc-500 shrink-0">Scale</span>
273
+ <span className="text-xs font-mono text-teal-700 dark:text-teal-400 ml-auto">
274
+ {georef.mapConversion.scale}
275
+ </span>
276
+ </div>
277
+ )}
278
+ </>
279
+ )}
280
+ </div>
281
+ </div>
282
+ )}
283
+
284
+ {/* IfcProject Data */}
285
+ {projectData && (
286
+ <div className="border-b border-zinc-200 dark:border-zinc-800">
287
+ <div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
288
+ <h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
289
+ Project Information
290
+ </h4>
291
+ </div>
292
+ <div className="divide-y divide-zinc-100 dark:divide-zinc-900">
293
+ {projectData.name && (
294
+ <div className="flex items-center gap-3 px-3 py-2">
295
+ <Tag className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
296
+ <span className="text-xs text-zinc-500">Name</span>
297
+ <span className="text-xs font-medium text-zinc-900 dark:text-zinc-100 ml-auto truncate max-w-[60%]">
298
+ {projectData.name}
299
+ </span>
300
+ </div>
301
+ )}
302
+ {projectData.description && (
303
+ <div className="flex items-start gap-3 px-3 py-2">
304
+ <FileText className="h-3.5 w-3.5 text-zinc-400 shrink-0 mt-0.5" />
305
+ <span className="text-xs text-zinc-500 shrink-0">Description</span>
306
+ <span className="text-xs text-zinc-900 dark:text-zinc-100 ml-auto text-right max-w-[60%]">
307
+ {projectData.description}
308
+ </span>
309
+ </div>
310
+ )}
311
+ {projectData.globalId && (
312
+ <div className="flex items-center gap-3 px-3 py-2">
313
+ <Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
314
+ <span className="text-xs text-zinc-500">GlobalId</span>
315
+ <code className="text-[10px] font-mono text-zinc-600 dark:text-zinc-400 ml-auto truncate max-w-[60%]">
316
+ {projectData.globalId}
317
+ </code>
318
+ </div>
319
+ )}
320
+ </div>
321
+
322
+ {/* Project Properties */}
323
+ {projectData.properties.length > 0 && (
324
+ <div className="p-3 pt-0 space-y-2">
325
+ {projectData.properties.map((pset) => (
326
+ <PropertySetCard key={pset.name} pset={pset} />
327
+ ))}
328
+ </div>
329
+ )}
330
+ </div>
331
+ )}
332
+ </ScrollArea>
333
+ </div>
334
+ );
335
+ }
@@ -0,0 +1,132 @@
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
+ * Property set display component with edit support.
7
+ */
8
+
9
+ import { Sparkles, PenLine } from 'lucide-react';
10
+ import { PropertyEditor } from '../PropertyEditor';
11
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
12
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
13
+ import { Badge } from '@/components/ui/badge';
14
+ import { decodeIfcString, parsePropertyValue } from './encodingUtils';
15
+ import type { PropertySet } from './encodingUtils';
16
+
17
+ export interface PropertySetCardProps {
18
+ pset: PropertySet;
19
+ modelId?: string;
20
+ entityId?: number;
21
+ enableEditing?: boolean;
22
+ }
23
+
24
+ export function PropertySetCard({ pset, modelId, entityId, enableEditing }: PropertySetCardProps) {
25
+ // Check if any property in this set is mutated
26
+ const hasMutations = pset.properties.some(p => p.isMutated);
27
+ const isNewPset = pset.isNewPset;
28
+
29
+ // Dynamic styling based on mutation state
30
+ const borderClass = isNewPset
31
+ ? 'border-2 border-amber-400/50 dark:border-amber-500/30'
32
+ : hasMutations
33
+ ? 'border-2 border-purple-300/50 dark:border-purple-500/30'
34
+ : 'border-2 border-zinc-200 dark:border-zinc-800';
35
+
36
+ const bgClass = isNewPset
37
+ ? 'bg-amber-50/30 dark:bg-amber-950/20'
38
+ : hasMutations
39
+ ? 'bg-purple-50/20 dark:bg-purple-950/10'
40
+ : 'bg-white dark:bg-zinc-950';
41
+
42
+ return (
43
+ <Collapsible defaultOpen className={`${borderClass} ${bgClass} group w-full max-w-full overflow-hidden`}>
44
+ <CollapsibleTrigger className="flex items-center gap-2 w-full p-2.5 hover:bg-zinc-50 dark:hover:bg-zinc-900 text-left transition-colors overflow-hidden">
45
+ {isNewPset && (
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <Sparkles className="h-3.5 w-3.5 text-amber-500 shrink-0" />
49
+ </TooltipTrigger>
50
+ <TooltipContent>New property set (not in original model)</TooltipContent>
51
+ </Tooltip>
52
+ )}
53
+ {hasMutations && !isNewPset && (
54
+ <Tooltip>
55
+ <TooltipTrigger asChild>
56
+ <PenLine className="h-3.5 w-3.5 text-purple-500 shrink-0" />
57
+ </TooltipTrigger>
58
+ <TooltipContent>Has modified properties</TooltipContent>
59
+ </Tooltip>
60
+ )}
61
+ <span className="font-bold text-xs text-zinc-900 dark:text-zinc-100 truncate flex-1 min-w-0">{decodeIfcString(pset.name)}</span>
62
+ <span className="text-[10px] font-mono bg-zinc-100 dark:bg-zinc-900 px-1.5 py-0.5 border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400 shrink-0">{pset.properties.length}</span>
63
+ </CollapsibleTrigger>
64
+ <CollapsibleContent>
65
+ <div className="border-t-2 border-zinc-200 dark:border-zinc-800 divide-y divide-zinc-100 dark:divide-zinc-900">
66
+ {pset.properties.map((prop: { name: string; value: unknown; isMutated?: boolean }) => {
67
+ const parsed = parsePropertyValue(prop.value);
68
+ const decodedName = decodeIfcString(prop.name);
69
+ const isMutated = prop.isMutated;
70
+
71
+ return (
72
+ <div
73
+ key={prop.name}
74
+ className={`flex items-start justify-between gap-2 px-3 py-2 text-xs group/prop ${
75
+ isMutated
76
+ ? 'bg-purple-50/50 dark:bg-purple-950/30 hover:bg-purple-100/50 dark:hover:bg-purple-900/30'
77
+ : 'hover:bg-zinc-50/50 dark:hover:bg-zinc-900/50'
78
+ }`}
79
+ >
80
+ <div className="flex flex-col gap-0.5 flex-1 min-w-0">
81
+ {/* Property name with type tooltip and mutation indicator */}
82
+ <div className="flex items-center gap-1.5">
83
+ {isMutated && (
84
+ <Tooltip>
85
+ <TooltipTrigger asChild>
86
+ <Badge variant="secondary" className="h-4 px-1 text-[9px] bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-700">
87
+ edited
88
+ </Badge>
89
+ </TooltipTrigger>
90
+ <TooltipContent>This property has been modified</TooltipContent>
91
+ </Tooltip>
92
+ )}
93
+ {parsed.ifcType ? (
94
+ <Tooltip>
95
+ <TooltipTrigger asChild>
96
+ <span className={`font-medium cursor-help break-words ${isMutated ? 'text-purple-600 dark:text-purple-400' : 'text-zinc-500 dark:text-zinc-400'}`}>
97
+ {decodedName}
98
+ </span>
99
+ </TooltipTrigger>
100
+ <TooltipContent side="top" className="text-[10px]">
101
+ <span className="text-zinc-400">{parsed.ifcType}</span>
102
+ </TooltipContent>
103
+ </Tooltip>
104
+ ) : (
105
+ <span className={`font-medium break-words ${isMutated ? 'text-purple-600 dark:text-purple-400' : 'text-zinc-500 dark:text-zinc-400'}`}>
106
+ {decodedName}
107
+ </span>
108
+ )}
109
+ </div>
110
+ {/* Property value - use PropertyEditor if editing enabled */}
111
+ {enableEditing && modelId && entityId ? (
112
+ <PropertyEditor
113
+ modelId={modelId}
114
+ entityId={entityId}
115
+ psetName={pset.name}
116
+ propName={prop.name}
117
+ currentValue={prop.value}
118
+ />
119
+ ) : (
120
+ <span className={`font-mono select-all break-words ${isMutated ? 'text-purple-900 dark:text-purple-100 font-semibold' : 'text-zinc-900 dark:text-zinc-100'}`}>
121
+ {parsed.displayValue}
122
+ </span>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ })}
128
+ </div>
129
+ </CollapsibleContent>
130
+ </Collapsible>
131
+ );
132
+ }
@@ -0,0 +1,79 @@
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
+ * Quantity set display component for IFC element quantities.
7
+ */
8
+
9
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
11
+ import { decodeIfcString } from './encodingUtils';
12
+ import type { QuantitySet } from './encodingUtils';
13
+
14
+ /** Maps quantity type to friendly name for tooltip */
15
+ const QUANTITY_TYPE_NAMES: Record<number, string> = {
16
+ 0: 'Length',
17
+ 1: 'Area',
18
+ 2: 'Volume',
19
+ 3: 'Count',
20
+ 4: 'Weight',
21
+ 5: 'Time',
22
+ };
23
+
24
+ export function QuantitySetCard({ qset }: { qset: QuantitySet }) {
25
+ const formatValue = (value: number, type: number): string => {
26
+ const formatted = value.toLocaleString(undefined, { maximumFractionDigits: 3 });
27
+ switch (type) {
28
+ case 0: return `${formatted} m`;
29
+ case 1: return `${formatted} m\u00B2`;
30
+ case 2: return `${formatted} m\u00B3`;
31
+ case 3: return formatted;
32
+ case 4: return `${formatted} kg`;
33
+ case 5: return `${formatted} s`;
34
+ default: return formatted;
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Collapsible defaultOpen className="border-2 border-blue-200 dark:border-blue-800 bg-blue-50/20 dark:bg-blue-950/20 w-full max-w-full overflow-hidden">
40
+ <CollapsibleTrigger className="flex items-center gap-2 w-full p-2.5 hover:bg-blue-50 dark:hover:bg-blue-900/30 text-left transition-colors overflow-hidden">
41
+ <span className="font-bold text-xs text-blue-700 dark:text-blue-400 truncate flex-1 min-w-0">{decodeIfcString(qset.name)}</span>
42
+ <span className="text-[10px] font-mono bg-blue-100 dark:bg-blue-900/50 px-1.5 py-0.5 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 shrink-0">{qset.quantities.length}</span>
43
+ </CollapsibleTrigger>
44
+ <CollapsibleContent>
45
+ <div className="border-t-2 border-blue-200 dark:border-blue-800 divide-y divide-blue-100 dark:divide-blue-900/30">
46
+ {qset.quantities.map((q: { name: string; value: number; type: number }, index: number) => {
47
+ const decodedName = decodeIfcString(q.name);
48
+ const typeName = QUANTITY_TYPE_NAMES[q.type];
49
+ return (
50
+ <div key={`${q.name}-${index}`} className="flex flex-col gap-0.5 px-3 py-2 text-xs hover:bg-blue-50/50 dark:hover:bg-blue-900/20">
51
+ {/* Quantity name with type tooltip */}
52
+ {typeName ? (
53
+ <Tooltip>
54
+ <TooltipTrigger asChild>
55
+ <span className="text-zinc-500 dark:text-zinc-400 font-medium cursor-help break-words">
56
+ {decodedName}
57
+ </span>
58
+ </TooltipTrigger>
59
+ <TooltipContent side="top" className="text-[10px]">
60
+ <span className="text-zinc-400">{typeName}</span>
61
+ </TooltipContent>
62
+ </Tooltip>
63
+ ) : (
64
+ <span className="text-zinc-500 dark:text-zinc-400 font-medium break-words">
65
+ {decodedName}
66
+ </span>
67
+ )}
68
+ {/* Quantity value */}
69
+ <span className="font-mono text-blue-700 dark:text-blue-400 select-all break-words">
70
+ {formatValue(q.value, q.type)}
71
+ </span>
72
+ </div>
73
+ );
74
+ })}
75
+ </div>
76
+ </CollapsibleContent>
77
+ </Collapsible>
78
+ );
79
+ }
@@ -0,0 +1,100 @@
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
+ * Relationships display component for IFC element structural relationships.
7
+ */
8
+
9
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
10
+ import { Link2 } from 'lucide-react';
11
+ import type { EntityRelationships } from '@ifc-lite/parser';
12
+
13
+ interface RelationshipsCardProps {
14
+ relationships: EntityRelationships;
15
+ onSelectEntity?: (entityId: number) => void;
16
+ }
17
+
18
+ export function RelationshipsCard({ relationships, onSelectEntity }: RelationshipsCardProps) {
19
+ const { voids, fills, groups, connections } = relationships;
20
+ const totalCount = voids.length + fills.length + groups.length + connections.length;
21
+
22
+ if (totalCount === 0) return null;
23
+
24
+ return (
25
+ <Collapsible defaultOpen className="border-2 border-zinc-300 dark:border-zinc-700 bg-zinc-50/20 dark:bg-zinc-950/20 w-full max-w-full overflow-hidden">
26
+ <CollapsibleTrigger className="flex items-center gap-2 w-full p-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800/30 text-left transition-colors overflow-hidden">
27
+ <Link2 className="h-3.5 w-3.5 text-zinc-600 dark:text-zinc-400 shrink-0" />
28
+ <span className="font-bold text-xs text-zinc-700 dark:text-zinc-300 truncate flex-1 min-w-0">
29
+ Relationships
30
+ </span>
31
+ <span className="text-[10px] font-mono bg-zinc-200 dark:bg-zinc-800 px-1.5 py-0.5 border border-zinc-300 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 shrink-0">
32
+ {totalCount}
33
+ </span>
34
+ </CollapsibleTrigger>
35
+ <CollapsibleContent>
36
+ <div className="border-t-2 border-zinc-300 dark:border-zinc-700 divide-y divide-zinc-200 dark:divide-zinc-800">
37
+ {voids.length > 0 && (
38
+ <div className="px-3 py-2">
39
+ <div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-1">
40
+ Openings ({voids.length})
41
+ </div>
42
+ {voids.map((item) => (
43
+ <RelItem key={item.id} item={item} onSelect={onSelectEntity} />
44
+ ))}
45
+ </div>
46
+ )}
47
+ {fills.length > 0 && (
48
+ <div className="px-3 py-2">
49
+ <div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-1">
50
+ Fills ({fills.length})
51
+ </div>
52
+ {fills.map((item) => (
53
+ <RelItem key={item.id} item={item} onSelect={onSelectEntity} />
54
+ ))}
55
+ </div>
56
+ )}
57
+ {groups.length > 0 && (
58
+ <div className="px-3 py-2">
59
+ <div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-1">
60
+ Groups ({groups.length})
61
+ </div>
62
+ {groups.map((item) => (
63
+ <div key={item.id} className="text-xs font-mono text-zinc-600 dark:text-zinc-400 py-0.5">
64
+ {item.name || `Group #${item.id}`}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ )}
69
+ {connections.length > 0 && (
70
+ <div className="px-3 py-2">
71
+ <div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider mb-1">
72
+ Connections ({connections.length})
73
+ </div>
74
+ {connections.map((item) => (
75
+ <RelItem key={item.id} item={item} onSelect={onSelectEntity} />
76
+ ))}
77
+ </div>
78
+ )}
79
+ </div>
80
+ </CollapsibleContent>
81
+ </Collapsible>
82
+ );
83
+ }
84
+
85
+ function RelItem({ item, onSelect }: {
86
+ item: { id: number; name?: string; type: string };
87
+ onSelect?: (id: number) => void;
88
+ }) {
89
+ return (
90
+ <button
91
+ className="flex items-center gap-2 text-xs py-0.5 w-full text-left hover:text-primary transition-colors"
92
+ onClick={() => onSelect?.(item.id)}
93
+ type="button"
94
+ >
95
+ <span className="font-mono text-zinc-500 dark:text-zinc-500 text-[10px]">#{item.id}</span>
96
+ <span className="text-zinc-600 dark:text-zinc-400 truncate">{item.name || item.type}</span>
97
+ <span className="text-[10px] text-zinc-400 ml-auto shrink-0">{item.type}</span>
98
+ </button>
99
+ );
100
+ }