@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,464 @@
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
+ import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';
6
+ import type { IfcDataStore } from '@ifc-lite/parser';
7
+ import type { FederatedModel } from '@/store';
8
+ import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
9
+
10
+ /** Helper to create elevation key (with 0.5m tolerance for matching) */
11
+ export function elevationKey(elevation: number): string {
12
+ return (Math.round(elevation * 2) / 2).toFixed(2);
13
+ }
14
+
15
+ /** Convert IfcTypeEnum to NodeType string */
16
+ export function getNodeType(ifcType: IfcTypeEnum): NodeType {
17
+ switch (ifcType) {
18
+ case IfcTypeEnum.IfcProject: return 'IfcProject';
19
+ case IfcTypeEnum.IfcSite: return 'IfcSite';
20
+ case IfcTypeEnum.IfcBuilding: return 'IfcBuilding';
21
+ case IfcTypeEnum.IfcBuildingStorey: return 'IfcBuildingStorey';
22
+ default: return 'element';
23
+ }
24
+ }
25
+
26
+ /** Build unified storey data for multi-model mode */
27
+ export function buildUnifiedStoreys(models: Map<string, FederatedModel>): UnifiedStorey[] {
28
+ if (models.size <= 1) return [];
29
+
30
+ const storeysByElevation = new Map<string, UnifiedStorey>();
31
+
32
+ for (const [modelId, model] of models) {
33
+ const dataStore = model.ifcDataStore;
34
+ if (!dataStore?.spatialHierarchy) continue;
35
+
36
+ const hierarchy = dataStore.spatialHierarchy;
37
+ const { byStorey, storeyElevations } = hierarchy;
38
+
39
+ for (const [storeyId, elements] of byStorey.entries()) {
40
+ const elevation = storeyElevations.get(storeyId) ?? 0;
41
+ const name = dataStore.entities.getName(storeyId) || `Storey #${storeyId}`;
42
+ const key = elevationKey(elevation);
43
+
44
+ const storeyData: StoreyData = {
45
+ modelId,
46
+ storeyId,
47
+ name,
48
+ elevation,
49
+ elements: elements as number[],
50
+ };
51
+
52
+ if (storeysByElevation.has(key)) {
53
+ const unified = storeysByElevation.get(key)!;
54
+ unified.storeys.push(storeyData);
55
+ unified.totalElements += elements.length;
56
+ if (name.length < unified.name.length) {
57
+ unified.name = name;
58
+ }
59
+ } else {
60
+ storeysByElevation.set(key, {
61
+ key,
62
+ name,
63
+ elevation,
64
+ storeys: [storeyData],
65
+ totalElements: elements.length,
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ return Array.from(storeysByElevation.values())
72
+ .sort((a, b) => b.elevation - a.elevation);
73
+ }
74
+
75
+ /** Get all element IDs for a unified storey (as global IDs) - optimized to avoid spread operator */
76
+ export function getUnifiedStoreyElements(
77
+ unifiedStorey: UnifiedStorey,
78
+ models: Map<string, FederatedModel>
79
+ ): number[] {
80
+ // Pre-calculate total length for single allocation
81
+ const totalLength = unifiedStorey.storeys.reduce((sum, s) => sum + s.elements.length, 0);
82
+ const allElements = new Array<number>(totalLength);
83
+ let idx = 0;
84
+ for (const storey of unifiedStorey.storeys) {
85
+ const model = models.get(storey.modelId);
86
+ const offset = model?.idOffset ?? 0;
87
+ // Direct assignment instead of spread for better performance
88
+ for (const id of storey.elements) {
89
+ allElements[idx++] = id + offset;
90
+ }
91
+ }
92
+ return allElements;
93
+ }
94
+
95
+ /** Recursively build spatial nodes (Project -> Site -> Building) */
96
+ function buildSpatialNodes(
97
+ spatialNode: SpatialNode,
98
+ modelId: string,
99
+ dataStore: IfcDataStore,
100
+ depth: number,
101
+ parentNodeId: string,
102
+ stopAtBuilding: boolean,
103
+ idOffset: number,
104
+ expandedNodes: Set<string>,
105
+ nodes: TreeNode[]
106
+ ): void {
107
+ const nodeId = `${parentNodeId}-${spatialNode.expressId}`;
108
+ const nodeType = getNodeType(spatialNode.type);
109
+ const isNodeExpanded = expandedNodes.has(nodeId);
110
+
111
+ // Skip storeys in multi-model mode (they're shown in unified list)
112
+ if (stopAtBuilding && nodeType === 'IfcBuildingStorey') {
113
+ return;
114
+ }
115
+
116
+ // For storeys, get elements from byStorey map
117
+ let elements: number[] = [];
118
+ if (nodeType === 'IfcBuildingStorey') {
119
+ elements = (dataStore.spatialHierarchy?.byStorey.get(spatialNode.expressId) as number[]) || [];
120
+ }
121
+
122
+ // Check if has children
123
+ // In stopAtBuilding mode, buildings have no children (storeys shown separately)
124
+ const hasNonStoreyChildren = spatialNode.children?.some(
125
+ (c: SpatialNode) => getNodeType(c.type) !== 'IfcBuildingStorey'
126
+ );
127
+ const hasChildren = stopAtBuilding
128
+ ? (nodeType !== 'IfcBuilding' && hasNonStoreyChildren)
129
+ : (spatialNode.children?.length > 0) || (nodeType === 'IfcBuildingStorey' && elements.length > 0);
130
+
131
+ nodes.push({
132
+ id: nodeId,
133
+ expressIds: [spatialNode.expressId],
134
+ modelIds: [modelId],
135
+ name: spatialNode.name || `${nodeType} #${spatialNode.expressId}`,
136
+ type: nodeType,
137
+ depth,
138
+ hasChildren,
139
+ isExpanded: isNodeExpanded,
140
+ isVisible: true, // Visibility computed lazily during render
141
+ elementCount: nodeType === 'IfcBuildingStorey' ? elements.length : undefined,
142
+ storeyElevation: spatialNode.elevation,
143
+ // Store idOffset for lazy visibility computation
144
+ _idOffset: idOffset,
145
+ });
146
+
147
+ if (isNodeExpanded) {
148
+ // Sort storeys by elevation descending
149
+ const sortedChildren = nodeType === 'IfcBuilding'
150
+ ? [...(spatialNode.children || [])].sort((a, b) => (b.elevation || 0) - (a.elevation || 0))
151
+ : spatialNode.children || [];
152
+
153
+ for (const child of sortedChildren) {
154
+ buildSpatialNodes(child, modelId, dataStore, depth + 1, nodeId, stopAtBuilding, idOffset, expandedNodes, nodes);
155
+ }
156
+
157
+ // For storeys (single-model only), add elements
158
+ if (!stopAtBuilding && nodeType === 'IfcBuildingStorey' && elements.length > 0) {
159
+ for (const elementId of elements) {
160
+ const globalId = elementId + idOffset;
161
+ const entityType = dataStore.entities?.getTypeName(elementId) || 'Unknown';
162
+ const entityName = dataStore.entities?.getName(elementId) || `${entityType} #${elementId}`;
163
+
164
+ nodes.push({
165
+ id: `element-${modelId}-${elementId}`,
166
+ expressIds: [globalId], // Store global ID for visibility operations
167
+ modelIds: [modelId],
168
+ name: entityName,
169
+ type: 'element',
170
+ depth: depth + 1,
171
+ hasChildren: false,
172
+ isExpanded: false,
173
+ isVisible: true, // Computed lazily during render
174
+ });
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ /** Build the complete tree data structure */
181
+ export function buildTreeData(
182
+ models: Map<string, FederatedModel>,
183
+ ifcDataStore: IfcDataStore | null | undefined,
184
+ expandedNodes: Set<string>,
185
+ isMultiModel: boolean,
186
+ unifiedStoreys: UnifiedStorey[]
187
+ ): TreeNode[] {
188
+ const nodes: TreeNode[] = [];
189
+
190
+ // Multi-model mode: unified storeys + MODELS section
191
+ if (isMultiModel) {
192
+ // 1. Add unified storeys at the top
193
+ for (const unified of unifiedStoreys) {
194
+ const storeyNodeId = `unified-${unified.key}`;
195
+ const isExpanded = expandedNodes.has(storeyNodeId);
196
+ const allStoreyIds = unified.storeys.map(s => s.storeyId);
197
+
198
+ nodes.push({
199
+ id: storeyNodeId,
200
+ expressIds: allStoreyIds,
201
+ modelIds: unified.storeys.map(s => s.modelId),
202
+ name: unified.name,
203
+ type: 'unified-storey',
204
+ depth: 0,
205
+ hasChildren: unified.totalElements > 0,
206
+ isExpanded,
207
+ isVisible: true, // Computed lazily during render
208
+ elementCount: unified.totalElements,
209
+ storeyElevation: unified.elevation,
210
+ });
211
+
212
+ // If expanded, show elements grouped by model
213
+ if (isExpanded) {
214
+ for (const storey of unified.storeys) {
215
+ const model = models.get(storey.modelId);
216
+ const modelName = model?.name || storey.modelId;
217
+ const offset = model?.idOffset ?? 0;
218
+
219
+ // Add model contribution header
220
+ const contribNodeId = `contrib-${storey.modelId}-${storey.storeyId}`;
221
+ const contribExpanded = expandedNodes.has(contribNodeId);
222
+
223
+ nodes.push({
224
+ id: contribNodeId,
225
+ expressIds: [storey.storeyId],
226
+ modelIds: [storey.modelId],
227
+ name: modelName,
228
+ type: 'model-header',
229
+ depth: 1,
230
+ hasChildren: storey.elements.length > 0,
231
+ isExpanded: contribExpanded,
232
+ isVisible: true, // Computed lazily during render
233
+ elementCount: storey.elements.length,
234
+ _idOffset: offset,
235
+ });
236
+
237
+ // If contribution expanded, show elements
238
+ if (contribExpanded) {
239
+ const dataStore = model?.ifcDataStore;
240
+ for (const elementId of storey.elements) {
241
+ const globalId = elementId + offset;
242
+ const entityType = dataStore?.entities?.getTypeName(elementId) || 'Unknown';
243
+ const entityName = dataStore?.entities?.getName(elementId) || `${entityType} #${elementId}`;
244
+
245
+ nodes.push({
246
+ id: `element-${storey.modelId}-${elementId}`,
247
+ expressIds: [globalId], // Store global ID for visibility operations
248
+ modelIds: [storey.modelId],
249
+ name: entityName,
250
+ type: 'element',
251
+ depth: 2,
252
+ hasChildren: false,
253
+ isExpanded: false,
254
+ isVisible: true, // Computed lazily during render
255
+ });
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // 2. Add MODELS section header
263
+ nodes.push({
264
+ id: 'models-header',
265
+ expressIds: [],
266
+ modelIds: [],
267
+ name: 'Models',
268
+ type: 'model-header',
269
+ depth: 0,
270
+ hasChildren: false,
271
+ isExpanded: false,
272
+ isVisible: true,
273
+ });
274
+
275
+ // 3. Add each model with Project -> Site -> Building (NO storeys)
276
+ for (const [modelId, model] of models) {
277
+ const modelNodeId = `model-${modelId}`;
278
+ const isModelExpanded = expandedNodes.has(modelNodeId);
279
+ const hasSpatialHierarchy = model.ifcDataStore?.spatialHierarchy?.project !== undefined;
280
+
281
+ nodes.push({
282
+ id: modelNodeId,
283
+ expressIds: [],
284
+ modelIds: [modelId],
285
+ name: model.name,
286
+ type: 'model-header',
287
+ depth: 0,
288
+ hasChildren: hasSpatialHierarchy,
289
+ isExpanded: isModelExpanded,
290
+ isVisible: model.visible,
291
+ elementCount: model.ifcDataStore?.entityCount,
292
+ });
293
+
294
+ // If expanded, show Project -> Site -> Building (stop at building, no storeys)
295
+ if (isModelExpanded && model.ifcDataStore?.spatialHierarchy?.project) {
296
+ buildSpatialNodes(
297
+ model.ifcDataStore.spatialHierarchy.project,
298
+ modelId,
299
+ model.ifcDataStore,
300
+ 1,
301
+ modelNodeId,
302
+ true, // stopAtBuilding = true
303
+ model.idOffset ?? 0,
304
+ expandedNodes,
305
+ nodes
306
+ );
307
+ }
308
+ }
309
+ } else if (models.size === 1) {
310
+ // Single model: show full spatial hierarchy (including storeys)
311
+ const [modelId, model] = Array.from(models.entries())[0];
312
+ if (model.ifcDataStore?.spatialHierarchy?.project) {
313
+ buildSpatialNodes(
314
+ model.ifcDataStore.spatialHierarchy.project,
315
+ modelId,
316
+ model.ifcDataStore,
317
+ 0,
318
+ 'root',
319
+ false, // stopAtBuilding = false (show full hierarchy)
320
+ model.idOffset ?? 0,
321
+ expandedNodes,
322
+ nodes
323
+ );
324
+ }
325
+ } else if (ifcDataStore?.spatialHierarchy?.project) {
326
+ // Legacy single-model mode (no offset)
327
+ buildSpatialNodes(
328
+ ifcDataStore.spatialHierarchy.project,
329
+ 'legacy',
330
+ ifcDataStore,
331
+ 0,
332
+ 'root',
333
+ false,
334
+ 0,
335
+ expandedNodes,
336
+ nodes
337
+ );
338
+ }
339
+
340
+ return nodes;
341
+ }
342
+
343
+ /** Build tree data grouped by IFC class instead of spatial hierarchy.
344
+ * Only includes entities that have geometry (visible in the 3D viewer).
345
+ * @param geometricIds Pre-computed set of global IDs with geometry (memoized by caller). */
346
+ export function buildTypeTree(
347
+ models: Map<string, FederatedModel>,
348
+ ifcDataStore: IfcDataStore | null | undefined,
349
+ expandedNodes: Set<string>,
350
+ isMultiModel: boolean,
351
+ geometricIds?: Set<number>,
352
+ ): TreeNode[] {
353
+ // Collect entities grouped by IFC class across all models
354
+ const typeGroups = new Map<string, Array<{ expressId: number; globalId: number; name: string; modelId: string }>>();
355
+
356
+ const processDataStore = (dataStore: IfcDataStore, modelId: string, idOffset: number) => {
357
+ for (let i = 0; i < dataStore.entities.count; i++) {
358
+ const expressId = dataStore.entities.expressId[i];
359
+ const globalId = expressId + idOffset;
360
+
361
+ // Only include entities that have geometry
362
+ if (geometricIds && geometricIds.size > 0 && !geometricIds.has(globalId)) continue;
363
+
364
+ const typeName = dataStore.entities.getTypeName(expressId) || 'Unknown';
365
+ const entityName = dataStore.entities.getName(expressId) || `${typeName} #${expressId}`;
366
+
367
+ if (!typeGroups.has(typeName)) {
368
+ typeGroups.set(typeName, []);
369
+ }
370
+ typeGroups.get(typeName)!.push({ expressId, globalId, name: entityName, modelId });
371
+ }
372
+ };
373
+
374
+ // Process all models
375
+ if (models.size > 0) {
376
+ for (const [modelId, model] of models) {
377
+ if (model.ifcDataStore) {
378
+ processDataStore(model.ifcDataStore, modelId, model.idOffset ?? 0);
379
+ }
380
+ }
381
+ } else if (ifcDataStore) {
382
+ processDataStore(ifcDataStore, 'legacy', 0);
383
+ }
384
+
385
+ // Sort types alphabetically
386
+ const sortedTypes = Array.from(typeGroups.keys()).sort();
387
+
388
+ const nodes: TreeNode[] = [];
389
+ for (const typeName of sortedTypes) {
390
+ const entities = typeGroups.get(typeName)!;
391
+ const groupNodeId = `type-${typeName}`;
392
+ const isExpanded = expandedNodes.has(groupNodeId);
393
+
394
+ // Store all globalIds on the group node so getNodeElements is O(1),
395
+ // avoiding a full entity scan when the group is collapsed.
396
+ const groupGlobalIds = entities.map(e => e.globalId);
397
+
398
+ nodes.push({
399
+ id: groupNodeId,
400
+ expressIds: groupGlobalIds,
401
+ modelIds: [],
402
+ name: typeName,
403
+ type: 'type-group',
404
+ depth: 0,
405
+ hasChildren: entities.length > 0,
406
+ isExpanded,
407
+ isVisible: true,
408
+ elementCount: entities.length,
409
+ });
410
+
411
+ if (isExpanded) {
412
+ // Sort elements by name within type group
413
+ entities.sort((a, b) => a.name.localeCompare(b.name));
414
+ for (const entity of entities) {
415
+ const suffix = isMultiModel ? ` [${models.get(entity.modelId)?.name || entity.modelId}]` : '';
416
+ nodes.push({
417
+ id: `element-${entity.modelId}-${entity.expressId}`,
418
+ expressIds: [entity.globalId],
419
+ modelIds: [entity.modelId],
420
+ name: entity.name + suffix,
421
+ type: 'element',
422
+ depth: 1,
423
+ hasChildren: false,
424
+ isExpanded: false,
425
+ isVisible: true,
426
+ });
427
+ }
428
+ }
429
+ }
430
+
431
+ return nodes;
432
+ }
433
+
434
+ /** Filter nodes based on search query */
435
+ export function filterNodes(nodes: TreeNode[], searchQuery: string): TreeNode[] {
436
+ if (!searchQuery.trim()) return nodes;
437
+ const query = searchQuery.toLowerCase();
438
+ return nodes.filter(node =>
439
+ node.name.toLowerCase().includes(query) ||
440
+ node.type.toLowerCase().includes(query)
441
+ );
442
+ }
443
+
444
+ /** Split filtered nodes into storeys and models sections (for multi-model mode) */
445
+ export function splitNodes(
446
+ filteredNodes: TreeNode[],
447
+ isMultiModel: boolean
448
+ ): { storeysNodes: TreeNode[]; modelsNodes: TreeNode[] } {
449
+ if (!isMultiModel) {
450
+ // Single model mode - all nodes go in storeys section (which is the full hierarchy)
451
+ return { storeysNodes: filteredNodes, modelsNodes: [] };
452
+ }
453
+
454
+ // Find the models-header index to split
455
+ const modelsHeaderIdx = filteredNodes.findIndex(n => n.id === 'models-header');
456
+ if (modelsHeaderIdx === -1) {
457
+ return { storeysNodes: filteredNodes, modelsNodes: [] };
458
+ }
459
+
460
+ return {
461
+ storeysNodes: filteredNodes.slice(0, modelsHeaderIdx),
462
+ modelsNodes: filteredNodes.slice(modelsHeaderIdx + 1), // Skip the models-header itself
463
+ };
464
+ }
@@ -0,0 +1,54 @@
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
+ /** Node types for the hierarchy tree */
6
+ export type NodeType =
7
+ | 'unified-storey' // Grouped storey across models (multi-model only)
8
+ | 'model-header' // Model visibility control (section header or individual model)
9
+ | 'IfcProject' // Project node
10
+ | 'IfcSite' // Site node
11
+ | 'IfcBuilding' // Building node
12
+ | 'IfcBuildingStorey' // Storey node
13
+ | 'type-group' // IFC type grouping header (e.g., "IfcWall (47)")
14
+ | 'element'; // Individual element
15
+
16
+ export interface TreeNode {
17
+ id: string; // Unique ID for the node (can be composite)
18
+ /** Express IDs this node represents (for elements/storeys) */
19
+ expressIds: number[];
20
+ /** Model IDs this node belongs to */
21
+ modelIds: string[];
22
+ name: string;
23
+ type: NodeType;
24
+ depth: number;
25
+ hasChildren: boolean;
26
+ isExpanded: boolean;
27
+ isVisible: boolean; // Note: For storeys, computed lazily during render for performance
28
+ elementCount?: number;
29
+ storeyElevation?: number;
30
+ /** Internal: ID offset for lazy visibility computation */
31
+ _idOffset?: number;
32
+ }
33
+
34
+ /** Data for a storey from a single model */
35
+ export interface StoreyData {
36
+ modelId: string;
37
+ storeyId: number;
38
+ name: string;
39
+ elevation: number;
40
+ elements: number[];
41
+ }
42
+
43
+ /** Unified storey grouping storeys from multiple models */
44
+ export interface UnifiedStorey {
45
+ key: string; // Elevation-based key for matching
46
+ name: string;
47
+ elevation: number;
48
+ storeys: StoreyData[];
49
+ totalElements: number;
50
+ }
51
+
52
+ // Spatial container types (Project/Site/Building) - these don't have direct visibility toggles
53
+ const SPATIAL_CONTAINER_TYPES: Set<NodeType> = new Set(['IfcProject', 'IfcSite', 'IfcBuilding']);
54
+ export const isSpatialContainer = (type: NodeType): boolean => SPATIAL_CONTAINER_TYPES.has(type);