@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
|
@@ -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);
|