@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.
@@ -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 { FederatedModel } from '@/store';
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 + offset;
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
- // 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
- }
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) || (nodeType === 'IfcBuildingStorey' && elements.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(child, modelId, dataStore, depth + 1, nodeId, stopAtBuilding, idOffset, expandedNodes, nodes);
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 + idOffset;
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: [globalId], // Store global ID for visibility operations
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 + offset;
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: [globalId], // Store global ID for visibility operations
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, idOffset: number) => {
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 + idOffset;
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, model.idOffset ?? 0);
470
+ processDataStore(model.ifcDataStore, modelId);
381
471
  }
382
472
  }
383
473
  } else if (ifcDataStore) {
384
- processDataStore(ifcDataStore, 'legacy', 0);
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: groupGlobalIds,
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.globalId],
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, idOffset: number) => {
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 + idOffset;
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
- instances.push({ expressId: instId, globalId: instGlobalId, name: instName, modelId });
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 + idOffset,
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, model.idOffset ?? 0);
600
+ processDataStore(model.ifcDataStore, modelId);
506
601
  }
507
602
  }
508
603
  } else if (ifcDataStore) {
509
- processDataStore(ifcDataStore, 'legacy', 0);
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: allInstanceGlobalIds,
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: instanceGlobalIds,
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.globalId],
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
- /** Express IDs this node represents (for elements/storeys) */
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 { FederatedModel } from '@/store';
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.expressIds;
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
- // Convert local expressIds to global IDs using model's idOffset
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
- // Try legacy dataStore first (no offset needed, IDs are already global)
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
- const offset = model.idOffset ?? 0;
257
- return localIds.map(id => id + offset);
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.expressIds;
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) => {