@ifc-lite/viewer 1.14.3 → 1.14.4
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 +11 -0
- package/dist/assets/{Arrow.dom-BgkZDIQm.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/{basketViewActivator-h_M3YbMW.js → basketViewActivator-BZcoCL3V.js} +1 -1
- package/dist/assets/{browser-CRQ0bPh1.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/{index-C4VVJRL-.js → index-D7nEDctQ.js} +4 -4
- package/dist/assets/{index-Be6XjVeM.js → index-DX-Qf5fA.js} +17153 -16920
- package/dist/assets/{native-bridge-DtcJqlOi.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-BJJVu9P2.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/HierarchyPanel.tsx +28 -13
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/dist/assets/index-DdwD4c-E.css +0 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { IfcTypeEnum, EntityFlags, RelationshipType, type SpatialNode } from '@ifc-lite/data';
|
|
6
6
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
7
|
-
import type
|
|
7
|
+
import { useViewerStore, type FederatedModel } from '@/store';
|
|
8
8
|
import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
|
|
9
9
|
|
|
10
10
|
/** Helper to create elevation key (with 0.5m tolerance for matching) */
|
|
@@ -19,10 +19,74 @@ export function getNodeType(ifcType: IfcTypeEnum): NodeType {
|
|
|
19
19
|
case IfcTypeEnum.IfcSite: return 'IfcSite';
|
|
20
20
|
case IfcTypeEnum.IfcBuilding: return 'IfcBuilding';
|
|
21
21
|
case IfcTypeEnum.IfcBuildingStorey: return 'IfcBuildingStorey';
|
|
22
|
+
case IfcTypeEnum.IfcSpace: return 'IfcSpace';
|
|
22
23
|
default: return 'element';
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function resolveTreeGlobalId(
|
|
28
|
+
modelId: string,
|
|
29
|
+
expressId: number,
|
|
30
|
+
models: Map<string, FederatedModel>
|
|
31
|
+
): number {
|
|
32
|
+
if (modelId === 'legacy' || !models.has(modelId)) {
|
|
33
|
+
return expressId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return useViewerStore.getState().toGlobalId(modelId, expressId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function collectDescendantSpaceElements(
|
|
40
|
+
spatialNode: SpatialNode,
|
|
41
|
+
hierarchy: IfcDataStore['spatialHierarchy'],
|
|
42
|
+
cache: Map<number, Set<number>>
|
|
43
|
+
): Set<number> {
|
|
44
|
+
const cached = cache.get(spatialNode.expressId);
|
|
45
|
+
if (cached) return cached;
|
|
46
|
+
|
|
47
|
+
const elementIds = new Set<number>();
|
|
48
|
+
|
|
49
|
+
for (const child of spatialNode.children || []) {
|
|
50
|
+
if (getNodeType(child.type) === 'IfcSpace') {
|
|
51
|
+
for (const elementId of hierarchy?.bySpace.get(child.expressId) ?? []) {
|
|
52
|
+
elementIds.add(elementId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const elementId of collectDescendantSpaceElements(child, hierarchy, cache)) {
|
|
57
|
+
elementIds.add(elementId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cache.set(spatialNode.expressId, elementIds);
|
|
62
|
+
return elementIds;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getSpatialNodeElements(
|
|
66
|
+
spatialNode: SpatialNode,
|
|
67
|
+
dataStore: IfcDataStore,
|
|
68
|
+
nodeType: NodeType,
|
|
69
|
+
descendantSpaceCache: Map<number, Set<number>>
|
|
70
|
+
): number[] {
|
|
71
|
+
if (nodeType === 'IfcSpace') {
|
|
72
|
+
return (dataStore.spatialHierarchy?.bySpace.get(spatialNode.expressId) as number[]) || [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (nodeType !== 'IfcBuildingStorey') {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const storeyElements =
|
|
80
|
+
(dataStore.spatialHierarchy?.byStorey.get(spatialNode.expressId) as number[]) || [];
|
|
81
|
+
const descendantSpaceElements = collectDescendantSpaceElements(
|
|
82
|
+
spatialNode,
|
|
83
|
+
dataStore.spatialHierarchy,
|
|
84
|
+
descendantSpaceCache
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return storeyElements.filter((elementId) => !descendantSpaceElements.has(elementId));
|
|
88
|
+
}
|
|
89
|
+
|
|
26
90
|
/** Build unified storey data for multi-model mode */
|
|
27
91
|
export function buildUnifiedStoreys(models: Map<string, FederatedModel>): UnifiedStorey[] {
|
|
28
92
|
if (models.size <= 1) return [];
|
|
@@ -82,11 +146,8 @@ export function getUnifiedStoreyElements(
|
|
|
82
146
|
const allElements = new Array<number>(totalLength);
|
|
83
147
|
let idx = 0;
|
|
84
148
|
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
149
|
for (const id of storey.elements) {
|
|
89
|
-
allElements[idx++] = id
|
|
150
|
+
allElements[idx++] = resolveTreeGlobalId(storey.modelId, id, models);
|
|
90
151
|
}
|
|
91
152
|
}
|
|
92
153
|
return allElements;
|
|
@@ -96,13 +157,15 @@ export function getUnifiedStoreyElements(
|
|
|
96
157
|
function buildSpatialNodes(
|
|
97
158
|
spatialNode: SpatialNode,
|
|
98
159
|
modelId: string,
|
|
160
|
+
models: Map<string, FederatedModel>,
|
|
99
161
|
dataStore: IfcDataStore,
|
|
100
162
|
depth: number,
|
|
101
163
|
parentNodeId: string,
|
|
102
164
|
stopAtBuilding: boolean,
|
|
103
165
|
idOffset: number,
|
|
104
166
|
expandedNodes: Set<string>,
|
|
105
|
-
nodes: TreeNode[]
|
|
167
|
+
nodes: TreeNode[],
|
|
168
|
+
descendantSpaceCache: Map<number, Set<number>>
|
|
106
169
|
): void {
|
|
107
170
|
const nodeId = `${parentNodeId}-${spatialNode.expressId}`;
|
|
108
171
|
const nodeType = getNodeType(spatialNode.type);
|
|
@@ -113,11 +176,7 @@ function buildSpatialNodes(
|
|
|
113
176
|
return;
|
|
114
177
|
}
|
|
115
178
|
|
|
116
|
-
|
|
117
|
-
let elements: number[] = [];
|
|
118
|
-
if (nodeType === 'IfcBuildingStorey') {
|
|
119
|
-
elements = (dataStore.spatialHierarchy?.byStorey.get(spatialNode.expressId) as number[]) || [];
|
|
120
|
-
}
|
|
179
|
+
const elements = getSpatialNodeElements(spatialNode, dataStore, nodeType, descendantSpaceCache);
|
|
121
180
|
|
|
122
181
|
// Check if has children
|
|
123
182
|
// In stopAtBuilding mode, buildings have no children (storeys shown separately)
|
|
@@ -126,11 +185,13 @@ function buildSpatialNodes(
|
|
|
126
185
|
);
|
|
127
186
|
const hasChildren = stopAtBuilding
|
|
128
187
|
? (nodeType !== 'IfcBuilding' && hasNonStoreyChildren)
|
|
129
|
-
: (spatialNode.children?.length > 0) ||
|
|
188
|
+
: (spatialNode.children?.length > 0) ||
|
|
189
|
+
((nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace') && elements.length > 0);
|
|
130
190
|
|
|
131
191
|
nodes.push({
|
|
132
192
|
id: nodeId,
|
|
133
193
|
expressIds: [spatialNode.expressId],
|
|
194
|
+
globalIds: [resolveTreeGlobalId(modelId, spatialNode.expressId, models)],
|
|
134
195
|
modelIds: [modelId],
|
|
135
196
|
name: (spatialNode.name && spatialNode.name.toLowerCase() !== 'unknown')
|
|
136
197
|
? spatialNode.name
|
|
@@ -140,7 +201,7 @@ function buildSpatialNodes(
|
|
|
140
201
|
hasChildren,
|
|
141
202
|
isExpanded: isNodeExpanded,
|
|
142
203
|
isVisible: true, // Visibility computed lazily during render
|
|
143
|
-
elementCount: nodeType === 'IfcBuildingStorey' ? elements.length : undefined,
|
|
204
|
+
elementCount: nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace' ? elements.length : undefined,
|
|
144
205
|
storeyElevation: spatialNode.elevation,
|
|
145
206
|
// Store idOffset for lazy visibility computation
|
|
146
207
|
_idOffset: idOffset,
|
|
@@ -153,22 +214,36 @@ function buildSpatialNodes(
|
|
|
153
214
|
: spatialNode.children || [];
|
|
154
215
|
|
|
155
216
|
for (const child of sortedChildren) {
|
|
156
|
-
buildSpatialNodes(
|
|
217
|
+
buildSpatialNodes(
|
|
218
|
+
child,
|
|
219
|
+
modelId,
|
|
220
|
+
models,
|
|
221
|
+
dataStore,
|
|
222
|
+
depth + 1,
|
|
223
|
+
nodeId,
|
|
224
|
+
stopAtBuilding,
|
|
225
|
+
idOffset,
|
|
226
|
+
expandedNodes,
|
|
227
|
+
nodes,
|
|
228
|
+
descendantSpaceCache
|
|
229
|
+
);
|
|
157
230
|
}
|
|
158
231
|
|
|
159
232
|
// For storeys (single-model only), add elements
|
|
160
|
-
if (!stopAtBuilding && nodeType === 'IfcBuildingStorey' && elements.length > 0) {
|
|
233
|
+
if (!stopAtBuilding && (nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace') && elements.length > 0) {
|
|
161
234
|
for (const elementId of elements) {
|
|
162
|
-
const globalId = elementId
|
|
235
|
+
const globalId = resolveTreeGlobalId(modelId, elementId, models);
|
|
163
236
|
const entityType = dataStore.entities?.getTypeName(elementId) || 'Unknown';
|
|
164
237
|
const entityName = dataStore.entities?.getName(elementId) || `${entityType} #${elementId}`;
|
|
165
238
|
|
|
166
239
|
nodes.push({
|
|
167
240
|
id: `element-${modelId}-${elementId}`,
|
|
168
|
-
expressIds: [
|
|
241
|
+
expressIds: [elementId],
|
|
242
|
+
globalIds: [globalId],
|
|
169
243
|
modelIds: [modelId],
|
|
170
244
|
name: entityName,
|
|
171
245
|
type: 'element',
|
|
246
|
+
ifcType: entityType,
|
|
172
247
|
depth: depth + 1,
|
|
173
248
|
hasChildren: false,
|
|
174
249
|
isExpanded: false,
|
|
@@ -200,6 +275,7 @@ export function buildTreeData(
|
|
|
200
275
|
nodes.push({
|
|
201
276
|
id: storeyNodeId,
|
|
202
277
|
expressIds: allStoreyIds,
|
|
278
|
+
globalIds: unified.storeys.map((s) => s.storeyId + (models.get(s.modelId)?.idOffset ?? 0)),
|
|
203
279
|
modelIds: unified.storeys.map(s => s.modelId),
|
|
204
280
|
name: unified.name,
|
|
205
281
|
type: 'unified-storey',
|
|
@@ -225,6 +301,7 @@ export function buildTreeData(
|
|
|
225
301
|
nodes.push({
|
|
226
302
|
id: contribNodeId,
|
|
227
303
|
expressIds: [storey.storeyId],
|
|
304
|
+
globalIds: [resolveTreeGlobalId(storey.modelId, storey.storeyId, models)],
|
|
228
305
|
modelIds: [storey.modelId],
|
|
229
306
|
name: modelName,
|
|
230
307
|
type: 'model-header',
|
|
@@ -240,16 +317,18 @@ export function buildTreeData(
|
|
|
240
317
|
if (contribExpanded) {
|
|
241
318
|
const dataStore = model?.ifcDataStore;
|
|
242
319
|
for (const elementId of storey.elements) {
|
|
243
|
-
const globalId = elementId
|
|
320
|
+
const globalId = resolveTreeGlobalId(storey.modelId, elementId, models);
|
|
244
321
|
const entityType = dataStore?.entities?.getTypeName(elementId) || 'Unknown';
|
|
245
322
|
const entityName = dataStore?.entities?.getName(elementId) || `${entityType} #${elementId}`;
|
|
246
323
|
|
|
247
324
|
nodes.push({
|
|
248
325
|
id: `element-${storey.modelId}-${elementId}`,
|
|
249
|
-
expressIds: [
|
|
326
|
+
expressIds: [elementId],
|
|
327
|
+
globalIds: [globalId],
|
|
250
328
|
modelIds: [storey.modelId],
|
|
251
329
|
name: entityName,
|
|
252
330
|
type: 'element',
|
|
331
|
+
ifcType: entityType,
|
|
253
332
|
depth: 2,
|
|
254
333
|
hasChildren: false,
|
|
255
334
|
isExpanded: false,
|
|
@@ -265,6 +344,7 @@ export function buildTreeData(
|
|
|
265
344
|
nodes.push({
|
|
266
345
|
id: 'models-header',
|
|
267
346
|
expressIds: [],
|
|
347
|
+
globalIds: [],
|
|
268
348
|
modelIds: [],
|
|
269
349
|
name: 'Models',
|
|
270
350
|
type: 'model-header',
|
|
@@ -283,6 +363,7 @@ export function buildTreeData(
|
|
|
283
363
|
nodes.push({
|
|
284
364
|
id: modelNodeId,
|
|
285
365
|
expressIds: [],
|
|
366
|
+
globalIds: [],
|
|
286
367
|
modelIds: [modelId],
|
|
287
368
|
name: model.name,
|
|
288
369
|
type: 'model-header',
|
|
@@ -295,16 +376,19 @@ export function buildTreeData(
|
|
|
295
376
|
|
|
296
377
|
// If expanded, show Project -> Site -> Building (stop at building, no storeys)
|
|
297
378
|
if (isModelExpanded && model.ifcDataStore?.spatialHierarchy?.project) {
|
|
379
|
+
const descendantSpaceCache = new Map<number, Set<number>>();
|
|
298
380
|
buildSpatialNodes(
|
|
299
381
|
model.ifcDataStore.spatialHierarchy.project,
|
|
300
382
|
modelId,
|
|
383
|
+
models,
|
|
301
384
|
model.ifcDataStore,
|
|
302
385
|
1,
|
|
303
386
|
modelNodeId,
|
|
304
387
|
true, // stopAtBuilding = true
|
|
305
388
|
model.idOffset ?? 0,
|
|
306
389
|
expandedNodes,
|
|
307
|
-
nodes
|
|
390
|
+
nodes,
|
|
391
|
+
descendantSpaceCache
|
|
308
392
|
);
|
|
309
393
|
}
|
|
310
394
|
}
|
|
@@ -312,30 +396,36 @@ export function buildTreeData(
|
|
|
312
396
|
// Single model: show full spatial hierarchy (including storeys)
|
|
313
397
|
const [modelId, model] = Array.from(models.entries())[0];
|
|
314
398
|
if (model.ifcDataStore?.spatialHierarchy?.project) {
|
|
399
|
+
const descendantSpaceCache = new Map<number, Set<number>>();
|
|
315
400
|
buildSpatialNodes(
|
|
316
401
|
model.ifcDataStore.spatialHierarchy.project,
|
|
317
402
|
modelId,
|
|
403
|
+
models,
|
|
318
404
|
model.ifcDataStore,
|
|
319
405
|
0,
|
|
320
406
|
'root',
|
|
321
407
|
false, // stopAtBuilding = false (show full hierarchy)
|
|
322
408
|
model.idOffset ?? 0,
|
|
323
409
|
expandedNodes,
|
|
324
|
-
nodes
|
|
410
|
+
nodes,
|
|
411
|
+
descendantSpaceCache
|
|
325
412
|
);
|
|
326
413
|
}
|
|
327
414
|
} else if (ifcDataStore?.spatialHierarchy?.project) {
|
|
328
415
|
// Legacy single-model mode (no offset)
|
|
416
|
+
const descendantSpaceCache = new Map<number, Set<number>>();
|
|
329
417
|
buildSpatialNodes(
|
|
330
418
|
ifcDataStore.spatialHierarchy.project,
|
|
331
419
|
'legacy',
|
|
420
|
+
models,
|
|
332
421
|
ifcDataStore,
|
|
333
422
|
0,
|
|
334
423
|
'root',
|
|
335
424
|
false,
|
|
336
425
|
0,
|
|
337
426
|
expandedNodes,
|
|
338
|
-
nodes
|
|
427
|
+
nodes,
|
|
428
|
+
descendantSpaceCache
|
|
339
429
|
);
|
|
340
430
|
}
|
|
341
431
|
|
|
@@ -355,10 +445,10 @@ export function buildTypeTree(
|
|
|
355
445
|
// Collect entities grouped by IFC class across all models
|
|
356
446
|
const typeGroups = new Map<string, Array<{ expressId: number; globalId: number; name: string; modelId: string }>>();
|
|
357
447
|
|
|
358
|
-
const processDataStore = (dataStore: IfcDataStore, modelId: string
|
|
448
|
+
const processDataStore = (dataStore: IfcDataStore, modelId: string) => {
|
|
359
449
|
for (let i = 0; i < dataStore.entities.count; i++) {
|
|
360
450
|
const expressId = dataStore.entities.expressId[i];
|
|
361
|
-
const globalId = expressId
|
|
451
|
+
const globalId = resolveTreeGlobalId(modelId, expressId, models);
|
|
362
452
|
|
|
363
453
|
// Only include entities that have geometry
|
|
364
454
|
if (geometricIds && geometricIds.size > 0 && !geometricIds.has(globalId)) continue;
|
|
@@ -377,11 +467,11 @@ export function buildTypeTree(
|
|
|
377
467
|
if (models.size > 0) {
|
|
378
468
|
for (const [modelId, model] of models) {
|
|
379
469
|
if (model.ifcDataStore) {
|
|
380
|
-
processDataStore(model.ifcDataStore, modelId
|
|
470
|
+
processDataStore(model.ifcDataStore, modelId);
|
|
381
471
|
}
|
|
382
472
|
}
|
|
383
473
|
} else if (ifcDataStore) {
|
|
384
|
-
processDataStore(ifcDataStore, 'legacy'
|
|
474
|
+
processDataStore(ifcDataStore, 'legacy');
|
|
385
475
|
}
|
|
386
476
|
|
|
387
477
|
// Sort types alphabetically
|
|
@@ -399,10 +489,12 @@ export function buildTypeTree(
|
|
|
399
489
|
|
|
400
490
|
nodes.push({
|
|
401
491
|
id: groupNodeId,
|
|
402
|
-
expressIds:
|
|
492
|
+
expressIds: entities.map((e) => e.expressId),
|
|
493
|
+
globalIds: groupGlobalIds,
|
|
403
494
|
modelIds: [],
|
|
404
495
|
name: typeName,
|
|
405
496
|
type: 'type-group',
|
|
497
|
+
ifcType: typeName,
|
|
406
498
|
depth: 0,
|
|
407
499
|
hasChildren: entities.length > 0,
|
|
408
500
|
isExpanded,
|
|
@@ -417,10 +509,12 @@ export function buildTypeTree(
|
|
|
417
509
|
const suffix = isMultiModel ? ` [${models.get(entity.modelId)?.name || entity.modelId}]` : '';
|
|
418
510
|
nodes.push({
|
|
419
511
|
id: `element-${entity.modelId}-${entity.expressId}`,
|
|
420
|
-
expressIds: [entity.
|
|
512
|
+
expressIds: [entity.expressId],
|
|
513
|
+
globalIds: [entity.globalId],
|
|
421
514
|
modelIds: [entity.modelId],
|
|
422
515
|
name: entity.name + suffix,
|
|
423
516
|
type: 'element',
|
|
517
|
+
ifcType: typeName,
|
|
424
518
|
depth: 1,
|
|
425
519
|
hasChildren: false,
|
|
426
520
|
isExpanded: false,
|
|
@@ -451,13 +545,13 @@ export function buildIfcTypeTree(
|
|
|
451
545
|
typeClassName: string; // e.g. "IfcWallType"
|
|
452
546
|
modelId: string;
|
|
453
547
|
globalId: number;
|
|
454
|
-
instances: Array<{ expressId: number; globalId: number; name: string; modelId: string }>;
|
|
548
|
+
instances: Array<{ expressId: number; globalId: number; name: string; modelId: string; ifcType: string }>;
|
|
455
549
|
}
|
|
456
550
|
|
|
457
551
|
// Group by type class name (e.g. "IfcWallType") → individual types
|
|
458
552
|
const typeClassGroups = new Map<string, TypeEntry[]>();
|
|
459
553
|
|
|
460
|
-
const processDataStore = (dataStore: IfcDataStore, modelId: string
|
|
554
|
+
const processDataStore = (dataStore: IfcDataStore, modelId: string) => {
|
|
461
555
|
if (!dataStore.relationships) return;
|
|
462
556
|
|
|
463
557
|
// Find all type entities (entities with IS_TYPE flag)
|
|
@@ -477,10 +571,11 @@ export function buildIfcTypeTree(
|
|
|
477
571
|
const instances: TypeEntry['instances'] = [];
|
|
478
572
|
|
|
479
573
|
for (const instId of instanceIds) {
|
|
480
|
-
const instGlobalId = instId
|
|
574
|
+
const instGlobalId = resolveTreeGlobalId(modelId, instId, models);
|
|
481
575
|
if (geometricIds && geometricIds.size > 0 && !geometricIds.has(instGlobalId)) continue;
|
|
482
576
|
const instName = dataStore.entities.getName(instId) || `#${instId}`;
|
|
483
|
-
|
|
577
|
+
const instIfcType = dataStore.entities.getTypeName(instId) || 'Unknown';
|
|
578
|
+
instances.push({ expressId: instId, globalId: instGlobalId, name: instName, modelId, ifcType: instIfcType });
|
|
484
579
|
}
|
|
485
580
|
|
|
486
581
|
const entry: TypeEntry = {
|
|
@@ -488,7 +583,7 @@ export function buildIfcTypeTree(
|
|
|
488
583
|
typeName,
|
|
489
584
|
typeClassName,
|
|
490
585
|
modelId,
|
|
491
|
-
globalId: expressId
|
|
586
|
+
globalId: resolveTreeGlobalId(modelId, expressId, models),
|
|
492
587
|
instances,
|
|
493
588
|
};
|
|
494
589
|
|
|
@@ -502,11 +597,11 @@ export function buildIfcTypeTree(
|
|
|
502
597
|
if (models.size > 0) {
|
|
503
598
|
for (const [modelId, model] of models) {
|
|
504
599
|
if (model.ifcDataStore) {
|
|
505
|
-
processDataStore(model.ifcDataStore, modelId
|
|
600
|
+
processDataStore(model.ifcDataStore, modelId);
|
|
506
601
|
}
|
|
507
602
|
}
|
|
508
603
|
} else if (ifcDataStore) {
|
|
509
|
-
processDataStore(ifcDataStore, 'legacy'
|
|
604
|
+
processDataStore(ifcDataStore, 'legacy');
|
|
510
605
|
}
|
|
511
606
|
|
|
512
607
|
const nodes: TreeNode[] = [];
|
|
@@ -526,10 +621,12 @@ export function buildIfcTypeTree(
|
|
|
526
621
|
|
|
527
622
|
nodes.push({
|
|
528
623
|
id: classNodeId,
|
|
529
|
-
expressIds:
|
|
624
|
+
expressIds: types.flatMap(t => t.instances.map(i => i.expressId)),
|
|
625
|
+
globalIds: allInstanceGlobalIds,
|
|
530
626
|
modelIds: [],
|
|
531
627
|
name: className,
|
|
532
628
|
type: 'type-group',
|
|
629
|
+
ifcType: className,
|
|
533
630
|
depth: 0,
|
|
534
631
|
hasChildren: types.length > 0,
|
|
535
632
|
isExpanded: isClassExpanded,
|
|
@@ -549,11 +646,13 @@ export function buildIfcTypeTree(
|
|
|
549
646
|
|
|
550
647
|
nodes.push({
|
|
551
648
|
id: typeNodeId,
|
|
552
|
-
expressIds:
|
|
649
|
+
expressIds: typeEntry.instances.map(i => i.expressId),
|
|
650
|
+
globalIds: instanceGlobalIds,
|
|
553
651
|
entityExpressId: typeEntry.typeExpressId,
|
|
554
652
|
modelIds: [typeEntry.modelId],
|
|
555
653
|
name: `${typeEntry.typeName}${suffix}`,
|
|
556
654
|
type: 'ifc-type',
|
|
655
|
+
ifcType: typeEntry.typeClassName,
|
|
557
656
|
depth: 1,
|
|
558
657
|
hasChildren: typeEntry.instances.length > 0,
|
|
559
658
|
isExpanded: isTypeExpanded,
|
|
@@ -567,10 +666,12 @@ export function buildIfcTypeTree(
|
|
|
567
666
|
const instSuffix = isMultiModel ? ` [${models.get(inst.modelId)?.name || inst.modelId}]` : '';
|
|
568
667
|
nodes.push({
|
|
569
668
|
id: `element-${inst.modelId}-${inst.expressId}`,
|
|
570
|
-
expressIds: [inst.
|
|
669
|
+
expressIds: [inst.expressId],
|
|
670
|
+
globalIds: [inst.globalId],
|
|
571
671
|
modelIds: [inst.modelId],
|
|
572
672
|
name: inst.name + instSuffix,
|
|
573
673
|
type: 'element',
|
|
674
|
+
ifcType: inst.ifcType,
|
|
574
675
|
depth: 2,
|
|
575
676
|
hasChildren: false,
|
|
576
677
|
isExpanded: false,
|
|
@@ -10,20 +10,25 @@ export type NodeType =
|
|
|
10
10
|
| 'IfcSite' // Site node
|
|
11
11
|
| 'IfcBuilding' // Building node
|
|
12
12
|
| 'IfcBuildingStorey' // Storey node
|
|
13
|
+
| 'IfcSpace' // Space node
|
|
13
14
|
| 'type-group' // IFC class grouping header (e.g., "IfcWall (47)")
|
|
14
15
|
| 'ifc-type' // IFC type entity node (e.g., "IfcWallType/W01")
|
|
15
16
|
| 'element'; // Individual element
|
|
16
17
|
|
|
17
18
|
export interface TreeNode {
|
|
18
19
|
id: string; // Unique ID for the node (can be composite)
|
|
19
|
-
/**
|
|
20
|
+
/** Local express IDs this node represents */
|
|
20
21
|
expressIds: number[];
|
|
22
|
+
/** Federated global IDs for selection/visibility operations */
|
|
23
|
+
globalIds: number[];
|
|
21
24
|
/** Structured entity expressId for selectable non-element nodes (for example IFC type entities) */
|
|
22
25
|
entityExpressId?: number;
|
|
23
26
|
/** Model IDs this node belongs to */
|
|
24
27
|
modelIds: string[];
|
|
25
28
|
name: string;
|
|
26
29
|
type: NodeType;
|
|
30
|
+
/** Actual IFC class for element rows and type groups */
|
|
31
|
+
ifcType?: string;
|
|
27
32
|
depth: number;
|
|
28
33
|
hasChildren: boolean;
|
|
29
34
|
isExpanded: boolean;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
6
6
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
7
7
|
import type { GeometryResult } from '@ifc-lite/geometry';
|
|
8
|
-
import type
|
|
8
|
+
import { useViewerStore, type FederatedModel } from '@/store';
|
|
9
9
|
import type { TreeNode, UnifiedStorey } from './types';
|
|
10
10
|
import {
|
|
11
11
|
buildUnifiedStoreys,
|
|
@@ -176,6 +176,12 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
176
176
|
[models, hasGeometrySource ? meshCount : 0]
|
|
177
177
|
);
|
|
178
178
|
|
|
179
|
+
const toGlobalIdsForModel = useCallback((modelId: string, expressIds: number[]): number[] => {
|
|
180
|
+
if (modelId === 'legacy') return expressIds;
|
|
181
|
+
const state = useViewerStore.getState();
|
|
182
|
+
return expressIds.map((expressId) => state.toGlobalId(modelId, expressId));
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
179
185
|
// Build the tree data structure based on grouping mode
|
|
180
186
|
// Note: hiddenEntities intentionally NOT in deps - visibility computed lazily for performance
|
|
181
187
|
const treeData = useMemo(
|
|
@@ -219,7 +225,7 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
219
225
|
const getNodeElements = useCallback((node: TreeNode): number[] => {
|
|
220
226
|
if (node.type === 'type-group' || node.type === 'ifc-type') {
|
|
221
227
|
// GlobalIds are pre-stored on the node during tree construction — O(1)
|
|
222
|
-
return node.
|
|
228
|
+
return node.globalIds;
|
|
223
229
|
}
|
|
224
230
|
if (node.type === 'unified-storey') {
|
|
225
231
|
// Get all elements from all models for this unified storey
|
|
@@ -234,34 +240,43 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
234
240
|
const model = models.get(modelId);
|
|
235
241
|
if (model?.ifcDataStore?.spatialHierarchy) {
|
|
236
242
|
const localIds = (model.ifcDataStore.spatialHierarchy.byStorey.get(storeyId) as number[]) || [];
|
|
237
|
-
|
|
238
|
-
const offset = model.idOffset ?? 0;
|
|
239
|
-
return localIds.map(id => id + offset);
|
|
243
|
+
return toGlobalIdsForModel(modelId, localIds);
|
|
240
244
|
}
|
|
241
245
|
} else if (node.type === 'IfcBuildingStorey') {
|
|
242
246
|
// Get storey elements
|
|
243
247
|
const storeyId = node.expressIds[0];
|
|
244
248
|
const modelId = node.modelIds[0];
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
if (ifcDataStore?.spatialHierarchy) {
|
|
250
|
+
if (modelId === 'legacy' && ifcDataStore?.spatialHierarchy) {
|
|
248
251
|
const elements = ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
|
|
249
252
|
if (elements) return elements as number[];
|
|
250
253
|
}
|
|
251
254
|
|
|
252
|
-
// Or from the model in federation - need to apply idOffset
|
|
253
255
|
const model = models.get(modelId);
|
|
254
256
|
if (model?.ifcDataStore?.spatialHierarchy) {
|
|
255
257
|
const localIds = (model.ifcDataStore.spatialHierarchy.byStorey.get(storeyId) as number[]) || [];
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
return toGlobalIdsForModel(modelId, localIds);
|
|
259
|
+
}
|
|
260
|
+
} else if (node.type === 'IfcSpace') {
|
|
261
|
+
const spaceId = node.expressIds[0];
|
|
262
|
+
const modelId = node.modelIds[0];
|
|
263
|
+
|
|
264
|
+
if (modelId === 'legacy' && ifcDataStore?.spatialHierarchy) {
|
|
265
|
+
const elements = ifcDataStore.spatialHierarchy.bySpace.get(spaceId) ?? [];
|
|
266
|
+
return [spaceId, ...(elements as number[])];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const model = models.get(modelId);
|
|
270
|
+
if (model?.ifcDataStore?.spatialHierarchy) {
|
|
271
|
+
const localIds = (model.ifcDataStore.spatialHierarchy.bySpace.get(spaceId) as number[]) || [];
|
|
272
|
+
return [...node.globalIds, ...toGlobalIdsForModel(modelId, localIds)];
|
|
258
273
|
}
|
|
259
274
|
} else if (node.type === 'element') {
|
|
260
|
-
return node.
|
|
275
|
+
return node.globalIds;
|
|
261
276
|
}
|
|
262
277
|
// Spatial containers (Project, Site, Building) and top-level models don't have direct element visibility toggle
|
|
263
278
|
return [];
|
|
264
|
-
}, [models, ifcDataStore, unifiedStoreys, getUnifiedStoreyElements]);
|
|
279
|
+
}, [models, ifcDataStore, unifiedStoreys, getUnifiedStoreyElements, toGlobalIdsForModel]);
|
|
265
280
|
|
|
266
281
|
// Persist grouping mode preference
|
|
267
282
|
const handleSetGroupingMode = useCallback((mode: GroupingMode) => {
|