@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.
- package/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -17,261 +17,31 @@ import {
|
|
|
17
17
|
MousePointer2,
|
|
18
18
|
ArrowUpDown,
|
|
19
19
|
FileBox,
|
|
20
|
-
Clock,
|
|
21
|
-
HardDrive,
|
|
22
|
-
Hash,
|
|
23
|
-
Database,
|
|
24
|
-
Edit3,
|
|
25
|
-
Sparkles,
|
|
26
20
|
PenLine,
|
|
27
21
|
Crosshair,
|
|
28
22
|
} from 'lucide-react';
|
|
29
|
-
import {
|
|
23
|
+
import { EditToolbar } from './PropertyEditor';
|
|
30
24
|
import { Button } from '@/components/ui/button';
|
|
31
25
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
32
26
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
33
27
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
34
28
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
35
|
-
import { Badge } from '@/components/ui/badge';
|
|
36
29
|
import { useViewerStore } from '@/store';
|
|
37
30
|
import { useIfc } from '@/hooks/useIfc';
|
|
38
31
|
import { IfcQuery } from '@ifc-lite/query';
|
|
39
32
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
40
|
-
import { extractPropertiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
33
|
+
import { extractPropertiesOnDemand, extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
41
34
|
import type { EntityRef, FederatedModel } from '@/store/types';
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Result of parsing a property value.
|
|
56
|
-
* Contains the display value and optional IFC type for tooltip.
|
|
57
|
-
*/
|
|
58
|
-
interface ParsedPropertyValue {
|
|
59
|
-
displayValue: string;
|
|
60
|
-
ifcType?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Map of IFC boolean enumeration values to human-readable text
|
|
65
|
-
*/
|
|
66
|
-
const BOOLEAN_MAP: Record<string, string> = {
|
|
67
|
-
'.T.': 'True',
|
|
68
|
-
'.F.': 'False',
|
|
69
|
-
'.U.': 'Unknown',
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Friendly names for common IFC types (shown in tooltips)
|
|
74
|
-
*/
|
|
75
|
-
const IFC_TYPE_DISPLAY_NAMES: Record<string, string> = {
|
|
76
|
-
'IFCBOOLEAN': 'Boolean',
|
|
77
|
-
'IFCLOGICAL': 'Logical',
|
|
78
|
-
'IFCIDENTIFIER': 'Identifier',
|
|
79
|
-
'IFCLABEL': 'Label',
|
|
80
|
-
'IFCTEXT': 'Text',
|
|
81
|
-
'IFCREAL': 'Real',
|
|
82
|
-
'IFCINTEGER': 'Integer',
|
|
83
|
-
'IFCPOSITIVELENGTHMEASURE': 'Length',
|
|
84
|
-
'IFCLENGTHMEASURE': 'Length',
|
|
85
|
-
'IFCAREAMEASURE': 'Area',
|
|
86
|
-
'IFCVOLUMEMEASURE': 'Volume',
|
|
87
|
-
'IFCMASSMEASURE': 'Mass',
|
|
88
|
-
'IFCTHERMALTRANSMITTANCEMEASURE': 'Thermal Transmittance',
|
|
89
|
-
'IFCPRESSUREMEASURE': 'Pressure',
|
|
90
|
-
'IFCFORCEMEASURE': 'Force',
|
|
91
|
-
'IFCPLANEANGLEMEASURE': 'Angle',
|
|
92
|
-
'IFCTIMEMEASURE': 'Time',
|
|
93
|
-
'IFCNORMALISEDRATIOMEASURE': 'Ratio',
|
|
94
|
-
'IFCRATIOMEASURE': 'Ratio',
|
|
95
|
-
'IFCPOSITIVERATIOMEASURE': 'Ratio',
|
|
96
|
-
'IFCCOUNTMEASURE': 'Count',
|
|
97
|
-
'IFCMONETARYMEASURE': 'Currency',
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Decode IFC STEP encoded strings.
|
|
102
|
-
* Handles:
|
|
103
|
-
* - \X2\XXXX\X0\ - Unicode hex encoding (e.g., \X2\00E4\X0\ → ä)
|
|
104
|
-
* - \X\XX\ - ISO-8859-1 hex encoding
|
|
105
|
-
* - \S\X - Extended ASCII with escape
|
|
106
|
-
*/
|
|
107
|
-
function decodeIfcString(str: string): string {
|
|
108
|
-
if (!str || typeof str !== 'string') return str;
|
|
109
|
-
|
|
110
|
-
let result = str;
|
|
111
|
-
|
|
112
|
-
// Decode \X2\XXXX\X0\ patterns (Unicode 2-byte hex, can have multiple chars)
|
|
113
|
-
// Pattern: \X2\ followed by hex pairs, ended by \X0\
|
|
114
|
-
result = result.replace(/\\X2\\([0-9A-Fa-f]+)\\X0\\/g, (_, hex) => {
|
|
115
|
-
// hex can be multiple 4-char sequences (e.g., "00E400FC" for "äü")
|
|
116
|
-
let decoded = '';
|
|
117
|
-
for (let i = 0; i < hex.length; i += 4) {
|
|
118
|
-
const charCode = parseInt(hex.substring(i, i + 4), 16);
|
|
119
|
-
if (!isNaN(charCode)) {
|
|
120
|
-
decoded += String.fromCharCode(charCode);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return decoded;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Decode \X4\XXXXXXXX\X0\ patterns (Unicode 4-byte hex for chars outside BMP)
|
|
127
|
-
result = result.replace(/\\X4\\([0-9A-Fa-f]+)\\X0\\/g, (_, hex) => {
|
|
128
|
-
let decoded = '';
|
|
129
|
-
for (let i = 0; i < hex.length; i += 8) {
|
|
130
|
-
const codePoint = parseInt(hex.substring(i, i + 8), 16);
|
|
131
|
-
if (!isNaN(codePoint)) {
|
|
132
|
-
decoded += String.fromCodePoint(codePoint);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return decoded;
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Decode \X\XX\ patterns (ISO-8859-1 single byte)
|
|
139
|
-
result = result.replace(/\\X\\([0-9A-Fa-f]{2})/g, (_, hex) => {
|
|
140
|
-
const charCode = parseInt(hex, 16);
|
|
141
|
-
return !isNaN(charCode) ? String.fromCharCode(charCode) : '';
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Decode \S\X patterns (Latin extended, offset by 128)
|
|
145
|
-
result = result.replace(/\\S\\(.)/g, (_, char) => {
|
|
146
|
-
return String.fromCharCode(char.charCodeAt(0) + 128);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Decode \P..\ code page switches (simplified - just remove them)
|
|
150
|
-
result = result.replace(/\\P[A-Z]?\\/g, '');
|
|
151
|
-
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Parse and format a property value for display.
|
|
157
|
-
* Handles:
|
|
158
|
-
* - TypedValues like [IFCIDENTIFIER, '100 x 150mm'] -> display '100 x 150mm', tooltip 'Identifier'
|
|
159
|
-
* - Boolean enums like '.T.' -> 'True'
|
|
160
|
-
* - IFC encoded strings with \X2\, \X\ escape sequences
|
|
161
|
-
* - Null/undefined -> '—'
|
|
162
|
-
* - Regular values -> string conversion
|
|
163
|
-
*/
|
|
164
|
-
function parsePropertyValue(value: unknown): ParsedPropertyValue {
|
|
165
|
-
// Handle null/undefined
|
|
166
|
-
if (value === null || value === undefined) {
|
|
167
|
-
return { displayValue: '—' };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Handle typed value arrays [IFCTYPENAME, actualValue]
|
|
171
|
-
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'string') {
|
|
172
|
-
const [ifcType, innerValue] = value;
|
|
173
|
-
const typeName = ifcType.toUpperCase();
|
|
174
|
-
const friendlyType = IFC_TYPE_DISPLAY_NAMES[typeName] || typeName.replace(/^IFC/, '');
|
|
175
|
-
|
|
176
|
-
// Recursively parse the inner value
|
|
177
|
-
const parsed = parsePropertyValue(innerValue);
|
|
178
|
-
return {
|
|
179
|
-
displayValue: parsed.displayValue,
|
|
180
|
-
ifcType: friendlyType,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Handle boolean enumeration values
|
|
185
|
-
if (typeof value === 'string') {
|
|
186
|
-
const upperVal = value.toUpperCase();
|
|
187
|
-
if (BOOLEAN_MAP[upperVal]) {
|
|
188
|
-
return { displayValue: BOOLEAN_MAP[upperVal], ifcType: 'Boolean' };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Handle string that contains typed value pattern (from String(array) conversion)
|
|
192
|
-
// Pattern: "IFCTYPENAME,actualValue" or just "IFCTYPENAME," (empty value)
|
|
193
|
-
const typedMatch = value.match(/^(IFC[A-Z0-9_]+),(.*)$/i);
|
|
194
|
-
if (typedMatch) {
|
|
195
|
-
const [, ifcType, innerValue] = typedMatch;
|
|
196
|
-
const typeName = ifcType.toUpperCase();
|
|
197
|
-
const friendlyType = IFC_TYPE_DISPLAY_NAMES[typeName] || typeName.replace(/^IFC/, '');
|
|
198
|
-
|
|
199
|
-
// Handle empty value after type
|
|
200
|
-
if (!innerValue || innerValue.trim() === '') {
|
|
201
|
-
return { displayValue: '—', ifcType: friendlyType };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Check if the inner value is a boolean
|
|
205
|
-
const upperInner = innerValue.toUpperCase().trim();
|
|
206
|
-
if (BOOLEAN_MAP[upperInner]) {
|
|
207
|
-
return { displayValue: BOOLEAN_MAP[upperInner], ifcType: friendlyType };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Decode IFC string encoding and return
|
|
211
|
-
return { displayValue: decodeIfcString(innerValue), ifcType: friendlyType };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Regular string - decode IFC encoding
|
|
215
|
-
return { displayValue: decodeIfcString(value) };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Handle native booleans
|
|
219
|
-
if (typeof value === 'boolean') {
|
|
220
|
-
return { displayValue: value ? 'True' : 'False', ifcType: 'Boolean' };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Handle numbers
|
|
224
|
-
if (typeof value === 'number') {
|
|
225
|
-
// Format numbers nicely (limit decimal places, use locale formatting)
|
|
226
|
-
const formatted = Number.isInteger(value)
|
|
227
|
-
? value.toLocaleString()
|
|
228
|
-
: value.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
229
|
-
return { displayValue: formatted };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Fallback for other types
|
|
233
|
-
return { displayValue: String(value) };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Inline coordinate value with dim axis label */
|
|
237
|
-
function CoordVal({ axis, value }: { axis: string; value: number }) {
|
|
238
|
-
return (
|
|
239
|
-
<span className="whitespace-nowrap"><span className="opacity-50">{axis}</span>{'\u2009'}{value.toFixed(3)}</span>
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Copyable coordinate row: label + values with copy button hugging the values */
|
|
244
|
-
function CoordRow({ label, values, primary, copyLabel, coordCopied, onCopy }: {
|
|
245
|
-
label: string;
|
|
246
|
-
values: { axis: string; value: number }[];
|
|
247
|
-
primary?: boolean;
|
|
248
|
-
copyLabel: string;
|
|
249
|
-
coordCopied: string | null;
|
|
250
|
-
onCopy: (label: string, text: string) => void;
|
|
251
|
-
}) {
|
|
252
|
-
const isCopied = coordCopied === copyLabel;
|
|
253
|
-
const copyText = values.map(v => v.value.toFixed(3)).join(', ');
|
|
254
|
-
return (
|
|
255
|
-
<div className="flex items-start gap-1.5 group min-w-0">
|
|
256
|
-
{label && (
|
|
257
|
-
<span className={`text-[9px] font-medium uppercase tracking-wider w-[34px] shrink-0 pt-px ${primary ? 'text-muted-foreground' : 'text-muted-foreground/50'}`}>
|
|
258
|
-
{label}
|
|
259
|
-
</span>
|
|
260
|
-
)}
|
|
261
|
-
<span className={`font-mono text-[10px] min-w-0 tabular-nums leading-relaxed ${primary ? 'text-foreground' : 'text-muted-foreground/60'}`}>
|
|
262
|
-
{values.map((v, i) => (
|
|
263
|
-
<span key={v.axis}>{i > 0 && <>{' '}</>}<CoordVal axis={v.axis} value={v.value} /></span>
|
|
264
|
-
))}
|
|
265
|
-
</span>
|
|
266
|
-
<button
|
|
267
|
-
className={`shrink-0 p-0.5 rounded mt-px transition-colors ${isCopied ? 'text-emerald-500' : 'text-muted-foreground/30 opacity-0 group-hover:opacity-100 hover:text-muted-foreground'}`}
|
|
268
|
-
onClick={(e) => { e.stopPropagation(); onCopy(copyLabel, copyText); }}
|
|
269
|
-
>
|
|
270
|
-
{isCopied ? <Check className="h-2.5 w-2.5" /> : <Copy className="h-2.5 w-2.5" />}
|
|
271
|
-
</button>
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
}
|
|
36
|
+
import { CoordVal, CoordRow } from './properties/CoordinateDisplay';
|
|
37
|
+
import { PropertySetCard } from './properties/PropertySetCard';
|
|
38
|
+
import { QuantitySetCard } from './properties/QuantitySetCard';
|
|
39
|
+
import { ModelMetadataPanel } from './properties/ModelMetadataPanel';
|
|
40
|
+
import { ClassificationCard } from './properties/ClassificationCard';
|
|
41
|
+
import { MaterialCard } from './properties/MaterialCard';
|
|
42
|
+
import { DocumentCard } from './properties/DocumentCard';
|
|
43
|
+
import { RelationshipsCard } from './properties/RelationshipsCard';
|
|
44
|
+
import type { PropertySet, QuantitySet } from './properties/encodingUtils';
|
|
275
45
|
|
|
276
46
|
export function PropertiesPanel() {
|
|
277
47
|
const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
|
|
@@ -424,7 +194,7 @@ export function PropertiesPanel() {
|
|
|
424
194
|
//
|
|
425
195
|
// The full coordinate pipeline is:
|
|
426
196
|
// 1. WASM extracts IFC positions (Z-up) and applies RTC offset (wasmRtcOffset, in Z-up)
|
|
427
|
-
// 2. Mesh collector converts Z-up
|
|
197
|
+
// 2. Mesh collector converts Z-up -> Y-up: newY = oldZ, newZ = -oldY
|
|
428
198
|
// 3. CoordinateHandler may apply additional originShift (in Y-up) for large coordinates
|
|
429
199
|
// 4. Multi-model alignment adjusts positions so all models share the first model's RTC frame
|
|
430
200
|
//
|
|
@@ -539,26 +309,15 @@ export function PropertiesPanel() {
|
|
|
539
309
|
modelId = '__legacy__';
|
|
540
310
|
}
|
|
541
311
|
|
|
542
|
-
// DEBUG: Log what we're working with
|
|
543
|
-
console.log('[PropertiesPanel] modelId:', modelId, 'expressId:', expressId, 'mutationVersion:', mutationVersion);
|
|
544
|
-
console.log('[PropertiesPanel] mutationViews keys:', [...mutationViews.keys()]);
|
|
545
|
-
|
|
546
312
|
// Try to get properties from mutation view first (handles both base and mutations)
|
|
547
313
|
const mutationView = modelId ? mutationViews.get(modelId) : null;
|
|
548
|
-
console.log('[PropertiesPanel] mutationView exists:', !!mutationView);
|
|
549
314
|
|
|
550
315
|
if (mutationView && expressId) {
|
|
551
|
-
// DEBUG: Log mutation view state
|
|
552
|
-
const allMutations = mutationView.getMutations();
|
|
553
|
-
console.log('[PropertiesPanel] All mutations in view:', allMutations.length, allMutations);
|
|
554
|
-
|
|
555
316
|
// Get merged properties from mutation view (base + mutations applied)
|
|
556
317
|
const mergedProps = mutationView.getForEntity(expressId);
|
|
557
|
-
console.log('[PropertiesPanel] mergedProps from getForEntity:', mergedProps.length, mergedProps);
|
|
558
318
|
|
|
559
319
|
// Get list of actual mutations to track which properties changed
|
|
560
320
|
const mutations = mutationView.getMutationsForEntity(expressId);
|
|
561
|
-
console.log('[PropertiesPanel] mutations for this entity:', mutations.length, mutations);
|
|
562
321
|
|
|
563
322
|
// Build a set of mutated property keys for quick lookup
|
|
564
323
|
const mutatedKeys = new Set<string>();
|
|
@@ -613,16 +372,109 @@ export function PropertiesPanel() {
|
|
|
613
372
|
}, [entityNode]);
|
|
614
373
|
|
|
615
374
|
// Build attributes array for display - must be before early return to maintain hook order
|
|
375
|
+
// Uses schema-aware extraction to show ALL string/enum attributes for the entity type.
|
|
616
376
|
// Note: GlobalId is intentionally excluded since it's shown in the dedicated GUID field above
|
|
617
377
|
const attributes = useMemo(() => {
|
|
618
378
|
if (!entityNode) return [];
|
|
619
|
-
|
|
620
|
-
if (entityNode.name) attrs.push({ name: 'Name', value: entityNode.name });
|
|
621
|
-
if (entityNode.description) attrs.push({ name: 'Description', value: entityNode.description });
|
|
622
|
-
if (entityNode.objectType) attrs.push({ name: 'ObjectType', value: entityNode.objectType });
|
|
623
|
-
return attrs;
|
|
379
|
+
return entityNode.allAttributes();
|
|
624
380
|
}, [entityNode]);
|
|
625
381
|
|
|
382
|
+
// Extract classifications for the selected entity from the IFC data store
|
|
383
|
+
const classifications = useMemo(() => {
|
|
384
|
+
if (!selectedEntity) return [];
|
|
385
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
386
|
+
if (!dataStore) return [];
|
|
387
|
+
return extractClassificationsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
|
|
388
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
389
|
+
|
|
390
|
+
// Extract materials for the selected entity from the IFC data store
|
|
391
|
+
const materialInfo = useMemo(() => {
|
|
392
|
+
if (!selectedEntity) return null;
|
|
393
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
394
|
+
if (!dataStore) return null;
|
|
395
|
+
return extractMaterialsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
|
|
396
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
397
|
+
|
|
398
|
+
// Extract documents for the selected entity from the IFC data store
|
|
399
|
+
const documents = useMemo(() => {
|
|
400
|
+
if (!selectedEntity) return [];
|
|
401
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
402
|
+
if (!dataStore) return [];
|
|
403
|
+
return extractDocumentsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
|
|
404
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
405
|
+
|
|
406
|
+
// Extract structural relationships (openings, fills, groups, connections)
|
|
407
|
+
const entityRelationships = useMemo(() => {
|
|
408
|
+
if (!selectedEntity) return null;
|
|
409
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
410
|
+
if (!dataStore) return null;
|
|
411
|
+
const rels = extractRelationshipsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
|
|
412
|
+
const totalCount = rels.voids.length + rels.fills.length + rels.groups.length + rels.connections.length;
|
|
413
|
+
return totalCount > 0 ? rels : null;
|
|
414
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
415
|
+
|
|
416
|
+
// Extract type-level properties (e.g., from IfcWallType's HasPropertySets)
|
|
417
|
+
const typeProperties = useMemo(() => {
|
|
418
|
+
if (!selectedEntity) return null;
|
|
419
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
420
|
+
if (!dataStore) return null;
|
|
421
|
+
const result = extractTypePropertiesOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
|
|
422
|
+
if (!result) return null;
|
|
423
|
+
// Convert to PropertySet format for PropertySetCard
|
|
424
|
+
return {
|
|
425
|
+
typeName: result.typeName,
|
|
426
|
+
typeId: result.typeId,
|
|
427
|
+
psets: result.properties.map(pset => ({
|
|
428
|
+
name: pset.name,
|
|
429
|
+
properties: pset.properties.map(p => ({ name: p.name, value: p.value, isMutated: false })),
|
|
430
|
+
isNewPset: false,
|
|
431
|
+
})),
|
|
432
|
+
};
|
|
433
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
434
|
+
|
|
435
|
+
// Merge instance-level and type-level property sets into a single unified list.
|
|
436
|
+
// - Same-named psets: instance properties take precedence, type-level props are appended (deduped by name)
|
|
437
|
+
// - Type-only psets: added to the main list (no separate "Type" section)
|
|
438
|
+
// This matches how reference IFC viewers display properties.
|
|
439
|
+
const mergedProperties: PropertySet[] = useMemo(() => {
|
|
440
|
+
if (!typeProperties || typeProperties.psets.length === 0) return properties;
|
|
441
|
+
if (properties.length === 0) return typeProperties.psets;
|
|
442
|
+
|
|
443
|
+
const instanceByName = new Map<string, PropertySet>();
|
|
444
|
+
for (const pset of properties) {
|
|
445
|
+
instanceByName.set(pset.name, pset);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Start with instance psets, merging type-level props into matching ones
|
|
449
|
+
const result: PropertySet[] = [];
|
|
450
|
+
const merged = new Set<string>();
|
|
451
|
+
|
|
452
|
+
for (const pset of properties) {
|
|
453
|
+
const typePset = typeProperties.psets.find(tp => tp.name === pset.name);
|
|
454
|
+
if (typePset) {
|
|
455
|
+
// Merge: add type-level properties not already present in instance pset
|
|
456
|
+
const existingNames = new Set(pset.properties.map(p => p.name));
|
|
457
|
+
const extraProps = typePset.properties.filter(p => !existingNames.has(p.name));
|
|
458
|
+
result.push({
|
|
459
|
+
...pset,
|
|
460
|
+
properties: [...pset.properties, ...extraProps],
|
|
461
|
+
});
|
|
462
|
+
merged.add(pset.name);
|
|
463
|
+
} else {
|
|
464
|
+
result.push(pset);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Add type-only psets that don't exist at instance level
|
|
469
|
+
for (const typePset of typeProperties.psets) {
|
|
470
|
+
if (!merged.has(typePset.name) && !instanceByName.has(typePset.name)) {
|
|
471
|
+
result.push(typePset);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return result;
|
|
476
|
+
}, [properties, typeProperties]);
|
|
477
|
+
|
|
626
478
|
// Model metadata display (when clicking top-level model in hierarchy)
|
|
627
479
|
if (selectedModelId) {
|
|
628
480
|
const selectedModel = models.get(selectedModelId);
|
|
@@ -915,20 +767,27 @@ export function PropertiesPanel() {
|
|
|
915
767
|
<TabsContent value="properties" className="m-0 p-3 overflow-hidden">
|
|
916
768
|
{/* Edit toolbar - only shown when edit mode is active */}
|
|
917
769
|
{editMode && selectedEntity && (
|
|
918
|
-
<
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
770
|
+
<EditToolbar
|
|
771
|
+
modelId={selectedEntity.modelId}
|
|
772
|
+
entityId={selectedEntity.expressId}
|
|
773
|
+
entityType={entityType}
|
|
774
|
+
existingPsets={mergedProperties.map(p => p.name)}
|
|
775
|
+
existingQtos={quantities.map(q => q.name)}
|
|
776
|
+
schemaVersion={activeDataStore?.schemaVersion}
|
|
777
|
+
/>
|
|
926
778
|
)}
|
|
927
|
-
{
|
|
779
|
+
{mergedProperties.length === 0 && classifications.length === 0 && !materialInfo && documents.length === 0 ? (
|
|
928
780
|
<p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">No property sets</p>
|
|
929
781
|
) : (
|
|
930
782
|
<div className="space-y-3 w-full overflow-hidden">
|
|
931
|
-
{
|
|
783
|
+
{/* Type badge - show which type this element inherits from */}
|
|
784
|
+
{typeProperties && typeProperties.psets.length > 0 && (
|
|
785
|
+
<div className="flex items-center gap-2 px-1 pb-0.5 text-[11px] text-indigo-600/70 dark:text-indigo-400/60">
|
|
786
|
+
<Building2 className="h-3 w-3 shrink-0" />
|
|
787
|
+
<span className="font-medium truncate">Type: {typeProperties.typeName}</span>
|
|
788
|
+
</div>
|
|
789
|
+
)}
|
|
790
|
+
{mergedProperties.map((pset: PropertySet) => (
|
|
932
791
|
<PropertySetCard
|
|
933
792
|
key={pset.name}
|
|
934
793
|
pset={pset}
|
|
@@ -937,6 +796,48 @@ export function PropertiesPanel() {
|
|
|
937
796
|
enableEditing={editMode}
|
|
938
797
|
/>
|
|
939
798
|
))}
|
|
799
|
+
|
|
800
|
+
{/* Classifications */}
|
|
801
|
+
{classifications.length > 0 && (
|
|
802
|
+
<>
|
|
803
|
+
{mergedProperties.length > 0 && (
|
|
804
|
+
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
|
|
805
|
+
)}
|
|
806
|
+
{classifications.map((classification, i) => (
|
|
807
|
+
<ClassificationCard key={`class-${i}`} classification={classification} />
|
|
808
|
+
))}
|
|
809
|
+
</>
|
|
810
|
+
)}
|
|
811
|
+
|
|
812
|
+
{/* Materials */}
|
|
813
|
+
{materialInfo && (
|
|
814
|
+
<>
|
|
815
|
+
{(mergedProperties.length > 0 || classifications.length > 0) && (
|
|
816
|
+
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
|
|
817
|
+
)}
|
|
818
|
+
<MaterialCard material={materialInfo} />
|
|
819
|
+
</>
|
|
820
|
+
)}
|
|
821
|
+
|
|
822
|
+
{/* Documents */}
|
|
823
|
+
{documents.length > 0 && (
|
|
824
|
+
<>
|
|
825
|
+
{(mergedProperties.length > 0 || classifications.length > 0 || materialInfo) && (
|
|
826
|
+
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
|
|
827
|
+
)}
|
|
828
|
+
{documents.map((doc, i) => (
|
|
829
|
+
<DocumentCard key={`doc-${i}`} document={doc} />
|
|
830
|
+
))}
|
|
831
|
+
</>
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{/* Relationships */}
|
|
835
|
+
{entityRelationships && (
|
|
836
|
+
<>
|
|
837
|
+
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
|
|
838
|
+
<RelationshipsCard relationships={entityRelationships} />
|
|
839
|
+
</>
|
|
840
|
+
)}
|
|
940
841
|
</div>
|
|
941
842
|
)}
|
|
942
843
|
</TabsContent>
|
|
@@ -1048,15 +949,11 @@ function EntityDataSection({
|
|
|
1048
949
|
return entityNode.quantities();
|
|
1049
950
|
}, [entityNode]);
|
|
1050
951
|
|
|
1051
|
-
// Get attributes
|
|
952
|
+
// Get attributes - uses schema-aware extraction to show ALL string/enum attributes
|
|
1052
953
|
// Note: GlobalId is intentionally excluded since it's shown in the dedicated GUID field above
|
|
1053
954
|
const attributes = useMemo(() => {
|
|
1054
955
|
if (!entityNode) return [];
|
|
1055
|
-
|
|
1056
|
-
if (entityNode.name) attrs.push({ name: 'Name', value: entityNode.name });
|
|
1057
|
-
if (entityNode.description) attrs.push({ name: 'Description', value: entityNode.description });
|
|
1058
|
-
if (entityNode.objectType) attrs.push({ name: 'ObjectType', value: entityNode.objectType });
|
|
1059
|
-
return attrs;
|
|
956
|
+
return entityNode.allAttributes();
|
|
1060
957
|
}, [entityNode]);
|
|
1061
958
|
|
|
1062
959
|
// Get elevation info
|
|
@@ -1161,392 +1058,3 @@ function EntityDataSection({
|
|
|
1161
1058
|
</div>
|
|
1162
1059
|
);
|
|
1163
1060
|
}
|
|
1164
|
-
|
|
1165
|
-
interface PropertySetCardProps {
|
|
1166
|
-
pset: PropertySet;
|
|
1167
|
-
modelId?: string;
|
|
1168
|
-
entityId?: number;
|
|
1169
|
-
enableEditing?: boolean;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function PropertySetCard({ pset, modelId, entityId, enableEditing }: PropertySetCardProps) {
|
|
1173
|
-
// Check if any property in this set is mutated
|
|
1174
|
-
const hasMutations = pset.properties.some(p => p.isMutated);
|
|
1175
|
-
const isNewPset = pset.isNewPset;
|
|
1176
|
-
|
|
1177
|
-
// Dynamic styling based on mutation state
|
|
1178
|
-
const borderClass = isNewPset
|
|
1179
|
-
? 'border-2 border-amber-400/50 dark:border-amber-500/30'
|
|
1180
|
-
: hasMutations
|
|
1181
|
-
? 'border-2 border-purple-300/50 dark:border-purple-500/30'
|
|
1182
|
-
: 'border-2 border-zinc-200 dark:border-zinc-800';
|
|
1183
|
-
|
|
1184
|
-
const bgClass = isNewPset
|
|
1185
|
-
? 'bg-amber-50/30 dark:bg-amber-950/20'
|
|
1186
|
-
: hasMutations
|
|
1187
|
-
? 'bg-purple-50/20 dark:bg-purple-950/10'
|
|
1188
|
-
: 'bg-white dark:bg-zinc-950';
|
|
1189
|
-
|
|
1190
|
-
return (
|
|
1191
|
-
<Collapsible defaultOpen className={`${borderClass} ${bgClass} group w-full max-w-full overflow-hidden`}>
|
|
1192
|
-
<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">
|
|
1193
|
-
{isNewPset && (
|
|
1194
|
-
<Tooltip>
|
|
1195
|
-
<TooltipTrigger asChild>
|
|
1196
|
-
<Sparkles className="h-3.5 w-3.5 text-amber-500 shrink-0" />
|
|
1197
|
-
</TooltipTrigger>
|
|
1198
|
-
<TooltipContent>New property set (not in original model)</TooltipContent>
|
|
1199
|
-
</Tooltip>
|
|
1200
|
-
)}
|
|
1201
|
-
{hasMutations && !isNewPset && (
|
|
1202
|
-
<Tooltip>
|
|
1203
|
-
<TooltipTrigger asChild>
|
|
1204
|
-
<PenLine className="h-3.5 w-3.5 text-purple-500 shrink-0" />
|
|
1205
|
-
</TooltipTrigger>
|
|
1206
|
-
<TooltipContent>Has modified properties</TooltipContent>
|
|
1207
|
-
</Tooltip>
|
|
1208
|
-
)}
|
|
1209
|
-
<span className="font-bold text-xs text-zinc-900 dark:text-zinc-100 truncate flex-1 min-w-0">{decodeIfcString(pset.name)}</span>
|
|
1210
|
-
<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>
|
|
1211
|
-
</CollapsibleTrigger>
|
|
1212
|
-
<CollapsibleContent>
|
|
1213
|
-
<div className="border-t-2 border-zinc-200 dark:border-zinc-800 divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
1214
|
-
{pset.properties.map((prop: { name: string; value: unknown; isMutated?: boolean }) => {
|
|
1215
|
-
const parsed = parsePropertyValue(prop.value);
|
|
1216
|
-
const decodedName = decodeIfcString(prop.name);
|
|
1217
|
-
const isMutated = prop.isMutated;
|
|
1218
|
-
|
|
1219
|
-
return (
|
|
1220
|
-
<div
|
|
1221
|
-
key={prop.name}
|
|
1222
|
-
className={`flex items-start justify-between gap-2 px-3 py-2 text-xs group/prop ${
|
|
1223
|
-
isMutated
|
|
1224
|
-
? 'bg-purple-50/50 dark:bg-purple-950/30 hover:bg-purple-100/50 dark:hover:bg-purple-900/30'
|
|
1225
|
-
: 'hover:bg-zinc-50/50 dark:hover:bg-zinc-900/50'
|
|
1226
|
-
}`}
|
|
1227
|
-
>
|
|
1228
|
-
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
|
1229
|
-
{/* Property name with type tooltip and mutation indicator */}
|
|
1230
|
-
<div className="flex items-center gap-1.5">
|
|
1231
|
-
{isMutated && (
|
|
1232
|
-
<Tooltip>
|
|
1233
|
-
<TooltipTrigger asChild>
|
|
1234
|
-
<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">
|
|
1235
|
-
edited
|
|
1236
|
-
</Badge>
|
|
1237
|
-
</TooltipTrigger>
|
|
1238
|
-
<TooltipContent>This property has been modified</TooltipContent>
|
|
1239
|
-
</Tooltip>
|
|
1240
|
-
)}
|
|
1241
|
-
{parsed.ifcType ? (
|
|
1242
|
-
<Tooltip>
|
|
1243
|
-
<TooltipTrigger asChild>
|
|
1244
|
-
<span className={`font-medium cursor-help break-words ${isMutated ? 'text-purple-600 dark:text-purple-400' : 'text-zinc-500 dark:text-zinc-400'}`}>
|
|
1245
|
-
{decodedName}
|
|
1246
|
-
</span>
|
|
1247
|
-
</TooltipTrigger>
|
|
1248
|
-
<TooltipContent side="top" className="text-[10px]">
|
|
1249
|
-
<span className="text-zinc-400">{parsed.ifcType}</span>
|
|
1250
|
-
</TooltipContent>
|
|
1251
|
-
</Tooltip>
|
|
1252
|
-
) : (
|
|
1253
|
-
<span className={`font-medium break-words ${isMutated ? 'text-purple-600 dark:text-purple-400' : 'text-zinc-500 dark:text-zinc-400'}`}>
|
|
1254
|
-
{decodedName}
|
|
1255
|
-
</span>
|
|
1256
|
-
)}
|
|
1257
|
-
</div>
|
|
1258
|
-
{/* Property value - use PropertyEditor if editing enabled */}
|
|
1259
|
-
{enableEditing && modelId && entityId ? (
|
|
1260
|
-
<PropertyEditor
|
|
1261
|
-
modelId={modelId}
|
|
1262
|
-
entityId={entityId}
|
|
1263
|
-
psetName={pset.name}
|
|
1264
|
-
propName={prop.name}
|
|
1265
|
-
currentValue={prop.value}
|
|
1266
|
-
/>
|
|
1267
|
-
) : (
|
|
1268
|
-
<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'}`}>
|
|
1269
|
-
{parsed.displayValue}
|
|
1270
|
-
</span>
|
|
1271
|
-
)}
|
|
1272
|
-
</div>
|
|
1273
|
-
</div>
|
|
1274
|
-
);
|
|
1275
|
-
})}
|
|
1276
|
-
</div>
|
|
1277
|
-
</CollapsibleContent>
|
|
1278
|
-
</Collapsible>
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
/** Maps quantity type to friendly name for tooltip */
|
|
1283
|
-
const QUANTITY_TYPE_NAMES: Record<number, string> = {
|
|
1284
|
-
0: 'Length',
|
|
1285
|
-
1: 'Area',
|
|
1286
|
-
2: 'Volume',
|
|
1287
|
-
3: 'Count',
|
|
1288
|
-
4: 'Weight',
|
|
1289
|
-
5: 'Time',
|
|
1290
|
-
};
|
|
1291
|
-
|
|
1292
|
-
function QuantitySetCard({ qset }: { qset: QuantitySet }) {
|
|
1293
|
-
const formatValue = (value: number, type: number): string => {
|
|
1294
|
-
const formatted = value.toLocaleString(undefined, { maximumFractionDigits: 3 });
|
|
1295
|
-
switch (type) {
|
|
1296
|
-
case 0: return `${formatted} m`;
|
|
1297
|
-
case 1: return `${formatted} m²`;
|
|
1298
|
-
case 2: return `${formatted} m³`;
|
|
1299
|
-
case 3: return formatted;
|
|
1300
|
-
case 4: return `${formatted} kg`;
|
|
1301
|
-
case 5: return `${formatted} s`;
|
|
1302
|
-
default: return formatted;
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
|
|
1306
|
-
return (
|
|
1307
|
-
<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">
|
|
1308
|
-
<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">
|
|
1309
|
-
<span className="font-bold text-xs text-blue-700 dark:text-blue-400 truncate flex-1 min-w-0">{decodeIfcString(qset.name)}</span>
|
|
1310
|
-
<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>
|
|
1311
|
-
</CollapsibleTrigger>
|
|
1312
|
-
<CollapsibleContent>
|
|
1313
|
-
<div className="border-t-2 border-blue-200 dark:border-blue-800 divide-y divide-blue-100 dark:divide-blue-900/30">
|
|
1314
|
-
{qset.quantities.map((q: { name: string; value: number; type: number }) => {
|
|
1315
|
-
const decodedName = decodeIfcString(q.name);
|
|
1316
|
-
const typeName = QUANTITY_TYPE_NAMES[q.type];
|
|
1317
|
-
return (
|
|
1318
|
-
<div key={q.name} className="flex flex-col gap-0.5 px-3 py-2 text-xs hover:bg-blue-50/50 dark:hover:bg-blue-900/20">
|
|
1319
|
-
{/* Quantity name with type tooltip */}
|
|
1320
|
-
{typeName ? (
|
|
1321
|
-
<Tooltip>
|
|
1322
|
-
<TooltipTrigger asChild>
|
|
1323
|
-
<span className="text-zinc-500 dark:text-zinc-400 font-medium cursor-help break-words">
|
|
1324
|
-
{decodedName}
|
|
1325
|
-
</span>
|
|
1326
|
-
</TooltipTrigger>
|
|
1327
|
-
<TooltipContent side="top" className="text-[10px]">
|
|
1328
|
-
<span className="text-zinc-400">{typeName}</span>
|
|
1329
|
-
</TooltipContent>
|
|
1330
|
-
</Tooltip>
|
|
1331
|
-
) : (
|
|
1332
|
-
<span className="text-zinc-500 dark:text-zinc-400 font-medium break-words">
|
|
1333
|
-
{decodedName}
|
|
1334
|
-
</span>
|
|
1335
|
-
)}
|
|
1336
|
-
{/* Quantity value */}
|
|
1337
|
-
<span className="font-mono text-blue-700 dark:text-blue-400 select-all break-words">
|
|
1338
|
-
{formatValue(q.value, q.type)}
|
|
1339
|
-
</span>
|
|
1340
|
-
</div>
|
|
1341
|
-
);
|
|
1342
|
-
})}
|
|
1343
|
-
</div>
|
|
1344
|
-
</CollapsibleContent>
|
|
1345
|
-
</Collapsible>
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
/** Model metadata panel - displays file info, schema version, entity counts, etc. */
|
|
1350
|
-
function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
1351
|
-
const dataStore = model.ifcDataStore;
|
|
1352
|
-
|
|
1353
|
-
// Format file size
|
|
1354
|
-
const formatFileSize = (bytes: number): string => {
|
|
1355
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
1356
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1357
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
|
-
// Format date
|
|
1361
|
-
const formatDate = (timestamp: number): string => {
|
|
1362
|
-
return new Date(timestamp).toLocaleString();
|
|
1363
|
-
};
|
|
1364
|
-
|
|
1365
|
-
// Get IfcProject data if available
|
|
1366
|
-
const projectData = useMemo(() => {
|
|
1367
|
-
if (!dataStore?.spatialHierarchy?.project) return null;
|
|
1368
|
-
const project = dataStore.spatialHierarchy.project;
|
|
1369
|
-
const projectId = project.expressId;
|
|
1370
|
-
|
|
1371
|
-
// Get project entity attributes
|
|
1372
|
-
const name = dataStore.entities.getName(projectId);
|
|
1373
|
-
const globalId = dataStore.entities.getGlobalId(projectId);
|
|
1374
|
-
const description = dataStore.entities.getDescription(projectId);
|
|
1375
|
-
|
|
1376
|
-
// Get project properties
|
|
1377
|
-
const properties: PropertySet[] = [];
|
|
1378
|
-
if (dataStore.properties) {
|
|
1379
|
-
for (const pset of dataStore.properties.getForEntity(projectId)) {
|
|
1380
|
-
properties.push({
|
|
1381
|
-
name: pset.name,
|
|
1382
|
-
properties: pset.properties.map(p => ({ name: p.name, value: p.value })),
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
return { name, globalId, description, properties };
|
|
1388
|
-
}, [dataStore]);
|
|
1389
|
-
|
|
1390
|
-
// Count storeys and elements
|
|
1391
|
-
const stats = useMemo(() => {
|
|
1392
|
-
if (!dataStore?.spatialHierarchy) {
|
|
1393
|
-
return { storeys: 0, elementsWithGeometry: 0 };
|
|
1394
|
-
}
|
|
1395
|
-
const storeys = dataStore.spatialHierarchy.byStorey.size;
|
|
1396
|
-
let elementsWithGeometry = 0;
|
|
1397
|
-
for (const elements of dataStore.spatialHierarchy.byStorey.values()) {
|
|
1398
|
-
elementsWithGeometry += (elements as number[]).length;
|
|
1399
|
-
}
|
|
1400
|
-
return { storeys, elementsWithGeometry };
|
|
1401
|
-
}, [dataStore]);
|
|
1402
|
-
|
|
1403
|
-
return (
|
|
1404
|
-
<div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
|
|
1405
|
-
{/* Header */}
|
|
1406
|
-
<div className="p-4 border-b-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black space-y-3">
|
|
1407
|
-
<div className="flex items-start gap-3">
|
|
1408
|
-
<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)]">
|
|
1409
|
-
<FileBox className="h-5 w-5 text-primary" />
|
|
1410
|
-
</div>
|
|
1411
|
-
<div className="flex-1 min-w-0 pt-0.5">
|
|
1412
|
-
<h3 className="font-bold text-sm truncate uppercase tracking-tight text-zinc-900 dark:text-zinc-100">
|
|
1413
|
-
{model.name}
|
|
1414
|
-
</h3>
|
|
1415
|
-
<p className="text-xs font-mono text-zinc-500 dark:text-zinc-400">IFC Model</p>
|
|
1416
|
-
</div>
|
|
1417
|
-
</div>
|
|
1418
|
-
|
|
1419
|
-
{/* Schema badge */}
|
|
1420
|
-
<div className="flex items-center gap-2">
|
|
1421
|
-
<span className="text-[10px] font-mono bg-primary/10 border border-primary/30 px-2 py-1 text-primary font-bold uppercase">
|
|
1422
|
-
{model.schemaVersion}
|
|
1423
|
-
</span>
|
|
1424
|
-
</div>
|
|
1425
|
-
</div>
|
|
1426
|
-
|
|
1427
|
-
<ScrollArea className="flex-1">
|
|
1428
|
-
{/* File Information */}
|
|
1429
|
-
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
1430
|
-
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
1431
|
-
<h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
|
|
1432
|
-
File Information
|
|
1433
|
-
</h4>
|
|
1434
|
-
</div>
|
|
1435
|
-
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
1436
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1437
|
-
<HardDrive className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1438
|
-
<span className="text-xs text-zinc-500">File Size</span>
|
|
1439
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1440
|
-
{formatFileSize(model.fileSize)}
|
|
1441
|
-
</span>
|
|
1442
|
-
</div>
|
|
1443
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1444
|
-
<Clock className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1445
|
-
<span className="text-xs text-zinc-500">Loaded At</span>
|
|
1446
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1447
|
-
{formatDate(model.loadedAt)}
|
|
1448
|
-
</span>
|
|
1449
|
-
</div>
|
|
1450
|
-
{dataStore && (
|
|
1451
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1452
|
-
<Clock className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1453
|
-
<span className="text-xs text-zinc-500">Parse Time</span>
|
|
1454
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1455
|
-
{dataStore.parseTime.toFixed(0)} ms
|
|
1456
|
-
</span>
|
|
1457
|
-
</div>
|
|
1458
|
-
)}
|
|
1459
|
-
</div>
|
|
1460
|
-
</div>
|
|
1461
|
-
|
|
1462
|
-
{/* Entity Statistics */}
|
|
1463
|
-
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
1464
|
-
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
1465
|
-
<h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
|
|
1466
|
-
Statistics
|
|
1467
|
-
</h4>
|
|
1468
|
-
</div>
|
|
1469
|
-
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
1470
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1471
|
-
<Database className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1472
|
-
<span className="text-xs text-zinc-500">Total Entities</span>
|
|
1473
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1474
|
-
{dataStore?.entityCount?.toLocaleString() ?? 'N/A'}
|
|
1475
|
-
</span>
|
|
1476
|
-
</div>
|
|
1477
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1478
|
-
<Layers className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1479
|
-
<span className="text-xs text-zinc-500">Building Storeys</span>
|
|
1480
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1481
|
-
{stats.storeys}
|
|
1482
|
-
</span>
|
|
1483
|
-
</div>
|
|
1484
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1485
|
-
<Building2 className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1486
|
-
<span className="text-xs text-zinc-500">Elements with Geometry</span>
|
|
1487
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1488
|
-
{stats.elementsWithGeometry.toLocaleString()}
|
|
1489
|
-
</span>
|
|
1490
|
-
</div>
|
|
1491
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1492
|
-
<Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1493
|
-
<span className="text-xs text-zinc-500">Max Express ID</span>
|
|
1494
|
-
<span className="text-xs font-mono text-zinc-900 dark:text-zinc-100 ml-auto">
|
|
1495
|
-
{model.maxExpressId.toLocaleString()}
|
|
1496
|
-
</span>
|
|
1497
|
-
</div>
|
|
1498
|
-
</div>
|
|
1499
|
-
</div>
|
|
1500
|
-
|
|
1501
|
-
{/* IfcProject Data */}
|
|
1502
|
-
{projectData && (
|
|
1503
|
-
<div className="border-b border-zinc-200 dark:border-zinc-800">
|
|
1504
|
-
<div className="p-3 bg-zinc-50 dark:bg-zinc-900/50">
|
|
1505
|
-
<h4 className="font-bold text-xs uppercase tracking-wide text-zinc-700 dark:text-zinc-300">
|
|
1506
|
-
Project Information
|
|
1507
|
-
</h4>
|
|
1508
|
-
</div>
|
|
1509
|
-
<div className="divide-y divide-zinc-100 dark:divide-zinc-900">
|
|
1510
|
-
{projectData.name && (
|
|
1511
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1512
|
-
<Tag className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1513
|
-
<span className="text-xs text-zinc-500">Name</span>
|
|
1514
|
-
<span className="text-xs font-medium text-zinc-900 dark:text-zinc-100 ml-auto truncate max-w-[60%]">
|
|
1515
|
-
{projectData.name}
|
|
1516
|
-
</span>
|
|
1517
|
-
</div>
|
|
1518
|
-
)}
|
|
1519
|
-
{projectData.description && (
|
|
1520
|
-
<div className="flex items-start gap-3 px-3 py-2">
|
|
1521
|
-
<FileText className="h-3.5 w-3.5 text-zinc-400 shrink-0 mt-0.5" />
|
|
1522
|
-
<span className="text-xs text-zinc-500 shrink-0">Description</span>
|
|
1523
|
-
<span className="text-xs text-zinc-900 dark:text-zinc-100 ml-auto text-right max-w-[60%]">
|
|
1524
|
-
{projectData.description}
|
|
1525
|
-
</span>
|
|
1526
|
-
</div>
|
|
1527
|
-
)}
|
|
1528
|
-
{projectData.globalId && (
|
|
1529
|
-
<div className="flex items-center gap-3 px-3 py-2">
|
|
1530
|
-
<Hash className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
1531
|
-
<span className="text-xs text-zinc-500">GlobalId</span>
|
|
1532
|
-
<code className="text-[10px] font-mono text-zinc-600 dark:text-zinc-400 ml-auto truncate max-w-[60%]">
|
|
1533
|
-
{projectData.globalId}
|
|
1534
|
-
</code>
|
|
1535
|
-
</div>
|
|
1536
|
-
)}
|
|
1537
|
-
</div>
|
|
1538
|
-
|
|
1539
|
-
{/* Project Properties */}
|
|
1540
|
-
{projectData.properties.length > 0 && (
|
|
1541
|
-
<div className="p-3 pt-0 space-y-2">
|
|
1542
|
-
{projectData.properties.map((pset) => (
|
|
1543
|
-
<PropertySetCard key={pset.name} pset={pset} />
|
|
1544
|
-
))}
|
|
1545
|
-
</div>
|
|
1546
|
-
)}
|
|
1547
|
-
</div>
|
|
1548
|
-
)}
|
|
1549
|
-
</ScrollArea>
|
|
1550
|
-
</div>
|
|
1551
|
-
);
|
|
1552
|
-
}
|