@ifc-lite/viewer 1.17.0 → 1.17.1

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 (28) hide show
  1. package/.turbo/turbo-build.log +11 -15
  2. package/.turbo/turbo-typecheck.log +41 -1
  3. package/CHANGELOG.md +10 -0
  4. package/dist/assets/{Arrow.dom-CcoDLP6E.js → Arrow.dom-DuPUrOxJ.js} +1 -1
  5. package/dist/assets/{basketViewActivator-FtbS__bG.js → basketViewActivator-DetjPnvt.js} +1 -1
  6. package/dist/assets/{browser-CXd3z0DO.js → browser-BQdwnOUt.js} +1 -1
  7. package/dist/assets/geometry.worker-Bjm-ukng.js +1 -0
  8. package/dist/assets/ifc-lite_bg-DD0A7Yow.wasm +0 -0
  9. package/dist/assets/{index-DqNiuQep.js → index-B3X21yXA.js} +4 -4
  10. package/dist/assets/{index-D99fzcwI.js → index-BybGZJTW.js} +13456 -13364
  11. package/dist/assets/{native-bridge-DjDj2M6p.js → native-bridge-CN0ZMR2t.js} +1 -1
  12. package/dist/assets/{wasm-bridge-CDTF4ZQc.js → wasm-bridge-D0bALkma.js} +1 -1
  13. package/dist/index.html +1 -1
  14. package/package.json +11 -11
  15. package/src/components/viewer/PropertiesPanel.tsx +6 -7
  16. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +70 -0
  17. package/src/components/viewer/hierarchy/treeDataBuilder.ts +35 -10
  18. package/src/components/viewer/hierarchy/types.ts +24 -2
  19. package/src/sdk/adapters/visibility-adapter.ts +7 -49
  20. package/src/store/basketVisibleSet.test.ts +73 -3
  21. package/src/store/basketVisibleSet.ts +46 -75
  22. package/src/utils/serverDataModel.test.ts +90 -0
  23. package/src/utils/serverDataModel.ts +22 -34
  24. package/src/utils/spatialHierarchy.test.ts +38 -0
  25. package/src/utils/spatialHierarchy.ts +13 -23
  26. package/dist/assets/ifc-lite-TI3u_Zyw.js +0 -7
  27. package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
  28. package/dist/assets/workerHelpers-G7llXNMi.js +0 -36
@@ -1,4 +1,4 @@
1
- import { _ as c, __tla as __tla_0 } from "./index-D99fzcwI.js";
1
+ import { _ as c, __tla as __tla_0 } from "./index-BybGZJTW.js";
2
2
  let m;
3
3
  let __tla = Promise.all([
4
4
  (()=>{
@@ -1 +1 @@
1
- import{I as f,a as m}from"./index-D99fzcwI.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
1
+ import{I as f,a as m}from"./index-BybGZJTW.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
package/dist/index.html CHANGED
@@ -49,7 +49,7 @@
49
49
  <meta name="theme-color" content="#7aa2f7">
50
50
  <meta name="msapplication-TileColor" content="#1a1b26">
51
51
  <meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
52
- <script type="module" crossorigin src="/assets/index-D99fzcwI.js"></script>
52
+ <script type="module" crossorigin src="/assets/index-BybGZJTW.js"></script>
53
53
  <link rel="stylesheet" crossorigin href="/assets/index-Ba4eoTe7.css">
54
54
  </head>
55
55
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifc-lite/viewer",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "IFC-Lite viewer application",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -44,22 +44,22 @@
44
44
  "zustand": "^4.4.0",
45
45
  "@ifc-lite/bcf": "^1.15.0",
46
46
  "@ifc-lite/cache": "^1.14.3",
47
+ "@ifc-lite/data": "^1.14.6",
47
48
  "@ifc-lite/drawing-2d": "^1.14.3",
48
- "@ifc-lite/data": "^1.14.5",
49
- "@ifc-lite/export": "^1.16.0",
50
- "@ifc-lite/geometry": "^1.14.4",
51
49
  "@ifc-lite/encoding": "^1.14.4",
52
- "@ifc-lite/ids": "^1.14.6",
50
+ "@ifc-lite/export": "^1.16.0",
51
+ "@ifc-lite/geometry": "^1.15.0",
52
+ "@ifc-lite/ids": "^1.14.7",
53
53
  "@ifc-lite/lens": "^1.14.3",
54
- "@ifc-lite/lists": "^1.14.6",
55
- "@ifc-lite/parser": "^2.1.3",
56
- "@ifc-lite/renderer": "^1.14.6",
54
+ "@ifc-lite/lists": "^1.14.7",
55
+ "@ifc-lite/mutations": "^1.14.3",
56
+ "@ifc-lite/parser": "^2.1.4",
57
57
  "@ifc-lite/query": "^1.14.4",
58
+ "@ifc-lite/renderer": "^1.14.6",
59
+ "@ifc-lite/sandbox": "^1.14.4",
58
60
  "@ifc-lite/server-client": "^1.14.3",
59
61
  "@ifc-lite/spatial": "^1.14.4",
60
- "@ifc-lite/sandbox": "^1.14.4",
61
- "@ifc-lite/wasm": "^1.14.5",
62
- "@ifc-lite/mutations": "^1.14.3"
62
+ "@ifc-lite/wasm": "^1.15.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@tailwindcss/postcss": "^4.1.18",
@@ -32,7 +32,7 @@ import { configureMutationView } from '@/utils/configureMutationView';
32
32
  import { IfcQuery } from '@ifc-lite/query';
33
33
  import { MutablePropertyView } from '@ifc-lite/mutations';
34
34
  import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
35
- import { EntityFlags, RelationshipType } from '@ifc-lite/data';
35
+ import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
36
36
  import type { EntityRef, FederatedModel } from '@/store/types';
37
37
 
38
38
  import { CoordVal, CoordRow } from './properties/CoordinateDisplay';
@@ -573,7 +573,7 @@ export function PropertiesPanel() {
573
573
  };
574
574
  }, [selectedEntity, model, ifcDataStore, mutationViews, mutationVersion]);
575
575
 
576
- // Spatial containment info for spatial containers (Project, Site, Building, Storey)
576
+ // Spatial containment info for spatial containers (Project, Facility, Part, Storey, Space)
577
577
  const spatialContainment = useMemo(() => {
578
578
  if (!selectedEntity) return null;
579
579
  const dataStore = model?.ifcDataStore ?? ifcDataStore;
@@ -583,9 +583,8 @@ export function PropertiesPanel() {
583
583
  const hierarchy = dataStore.spatialHierarchy;
584
584
  const typeName = dataStore.entities.getTypeName(expressId);
585
585
 
586
- // Only show for spatial containers
587
- const spatialTypes = ['IfcProject', 'IfcSite', 'IfcBuilding', 'IfcBuildingStorey', 'IfcSpace'];
588
- if (!spatialTypes.includes(typeName)) return null;
586
+ // Only show for spatial structure elements.
587
+ if (!isSpatialStructureTypeName(typeName)) return null;
589
588
 
590
589
  const stats: Array<{ label: string; value: string | number }> = [];
591
590
 
@@ -621,7 +620,7 @@ export function PropertiesPanel() {
621
620
  // Also count from containment maps
622
621
  const mapSources: Array<[string, Map<number, number[]> | undefined]> = [
623
622
  ['Elements (Site)', hierarchy.bySite],
624
- ['Elements (Building)', hierarchy.byBuilding],
623
+ ['Elements (Building-like)', hierarchy.byBuilding],
625
624
  ['Elements (Storey)', hierarchy.byStorey],
626
625
  ['Elements (Space)', hierarchy.bySpace],
627
626
  ];
@@ -633,7 +632,7 @@ export function PropertiesPanel() {
633
632
  }
634
633
 
635
634
  // Elevation for storeys
636
- if (typeName === 'IfcBuildingStorey') {
635
+ if (isStoreyLikeSpatialTypeName(typeName)) {
637
636
  const elevation = hierarchy.storeyElevations.get(expressId);
638
637
  if (elevation !== undefined) {
639
638
  stats.push({ label: 'Elevation', value: `${elevation.toFixed(2)} m` });
@@ -65,6 +65,40 @@ function createDataStore(): IfcDataStore {
65
65
  } as unknown as IfcDataStore;
66
66
  }
67
67
 
68
+ function createFacilityDataStore(): IfcDataStore {
69
+ const partNode = createSpatialNode(3, IfcTypeEnum.IfcBridgePart, 'DECK');
70
+ partNode.elements = [4];
71
+ const bridgeNode = createSpatialNode(2, IfcTypeEnum.IfcBridge, 'BRIDGE', [partNode]);
72
+ const projectNode = createSpatialNode(1, IfcTypeEnum.IfcProject, 'INFRA_PROJECT', [bridgeNode]);
73
+
74
+ const spatialHierarchy: SpatialHierarchy = {
75
+ project: projectNode,
76
+ byStorey: new Map(),
77
+ byBuilding: new Map([[2, []]]),
78
+ bySite: new Map(),
79
+ bySpace: new Map(),
80
+ storeyElevations: new Map(),
81
+ storeyHeights: new Map(),
82
+ elementToStorey: new Map(),
83
+ getStoreyElements: () => [],
84
+ getStoreyByElevation: () => null,
85
+ getContainingSpace: () => null,
86
+ getPath: () => [],
87
+ };
88
+
89
+ return {
90
+ spatialHierarchy,
91
+ entities: {
92
+ count: 0,
93
+ getName: (id: number) => (id === 4 ? 'Barrier' : ''),
94
+ getTypeName: (id: number) => {
95
+ if (id === 4) return 'IfcWall';
96
+ return 'Unknown';
97
+ },
98
+ },
99
+ } as unknown as IfcDataStore;
100
+ }
101
+
68
102
  function createModel(idOffset: number): FederatedModel {
69
103
  return {
70
104
  id: 'model-1',
@@ -123,4 +157,40 @@ describe('buildTreeData', () => {
123
157
  assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-6').length, 1);
124
158
  assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-7').length, 1);
125
159
  });
160
+
161
+ it('keeps IFC4.3 facility and facility-part nodes as spatial hierarchy rows', () => {
162
+ useViewerStore.setState({ models: new Map() });
163
+ useViewerStore.getState().registerModelOffset('tree-test-infra-padding', 199);
164
+ const idOffset = useViewerStore.getState().registerModelOffset('model-infra', 4);
165
+ const model = {
166
+ ...createModel(idOffset),
167
+ id: 'model-infra',
168
+ name: 'Infra Model',
169
+ ifcDataStore: createFacilityDataStore(),
170
+ maxExpressId: 4,
171
+ };
172
+ useViewerStore.setState({ models: new Map([['model-infra', model]]) });
173
+
174
+ const nodes = buildTreeData(
175
+ new Map<string, FederatedModel>([['model-infra', model]]),
176
+ null,
177
+ new Set(['root-1', 'root-1-2', 'root-1-2-3']),
178
+ false,
179
+ [],
180
+ );
181
+
182
+ const bridgeNode = nodes.find((node) => node.id === 'root-1-2');
183
+ assert.ok(bridgeNode);
184
+ assert.strictEqual(bridgeNode.type, 'IfcBridge');
185
+
186
+ const partNode = nodes.find((node) => node.id === 'root-1-2-3');
187
+ assert.ok(partNode);
188
+ assert.strictEqual(partNode.type, 'IfcBridgePart');
189
+ assert.strictEqual(partNode.elementCount, 1);
190
+
191
+ const barrierNode = nodes.find((node) => node.id === 'element-model-infra-4');
192
+ assert.ok(barrierNode);
193
+ assert.strictEqual(barrierNode.type, 'element');
194
+ assert.strictEqual(barrierNode.ifcType, 'IfcWall');
195
+ });
126
196
  });
@@ -2,7 +2,15 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import { IfcTypeEnum, EntityFlags, RelationshipType, type SpatialNode } from '@ifc-lite/data';
5
+ import {
6
+ IfcTypeEnum,
7
+ EntityFlags,
8
+ RelationshipType,
9
+ isSpaceLikeSpatialType,
10
+ isSpatialStructureType,
11
+ isStoreyLikeSpatialType,
12
+ type SpatialNode,
13
+ } from '@ifc-lite/data';
6
14
  import type { IfcDataStore } from '@ifc-lite/parser';
7
15
  import { useViewerStore, type FederatedModel } from '@/store';
8
16
  import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
@@ -18,7 +26,16 @@ export function getNodeType(ifcType: IfcTypeEnum): NodeType {
18
26
  case IfcTypeEnum.IfcProject: return 'IfcProject';
19
27
  case IfcTypeEnum.IfcSite: return 'IfcSite';
20
28
  case IfcTypeEnum.IfcBuilding: return 'IfcBuilding';
29
+ case IfcTypeEnum.IfcFacility: return 'IfcFacility';
30
+ case IfcTypeEnum.IfcBridge: return 'IfcBridge';
31
+ case IfcTypeEnum.IfcRoad: return 'IfcRoad';
32
+ case IfcTypeEnum.IfcRailway: return 'IfcRailway';
33
+ case IfcTypeEnum.IfcMarineFacility: return 'IfcMarineFacility';
21
34
  case IfcTypeEnum.IfcBuildingStorey: return 'IfcBuildingStorey';
35
+ case IfcTypeEnum.IfcFacilityPart: return 'IfcFacilityPart';
36
+ case IfcTypeEnum.IfcBridgePart: return 'IfcBridgePart';
37
+ case IfcTypeEnum.IfcRoadPart: return 'IfcRoadPart';
38
+ case IfcTypeEnum.IfcRailwayPart: return 'IfcRailwayPart';
22
39
  case IfcTypeEnum.IfcSpace: return 'IfcSpace';
23
40
  default: return 'element';
24
41
  }
@@ -68,10 +85,17 @@ function getSpatialNodeElements(
68
85
  nodeType: NodeType,
69
86
  descendantSpaceCache: Map<number, Set<number>>
70
87
  ): number[] {
71
- if (nodeType === 'IfcSpace') {
88
+ if (isSpaceLikeSpatialType(spatialNode.type)) {
72
89
  return (dataStore.spatialHierarchy?.bySpace.get(spatialNode.expressId) as number[]) || [];
73
90
  }
74
91
 
92
+ if (!isStoreyLikeSpatialType(spatialNode.type)) {
93
+ if (!isSpatialStructureType(spatialNode.type)) {
94
+ return [];
95
+ }
96
+ return spatialNode.elements || [];
97
+ }
98
+
75
99
  if (nodeType !== 'IfcBuildingStorey') {
76
100
  return [];
77
101
  }
@@ -177,16 +201,16 @@ function buildSpatialNodes(
177
201
  }
178
202
 
179
203
  const elements = getSpatialNodeElements(spatialNode, dataStore, nodeType, descendantSpaceCache);
204
+ const hasDirectElements = elements.length > 0;
180
205
 
181
206
  // Check if has children
182
207
  // In stopAtBuilding mode, buildings have no children (storeys shown separately)
183
208
  const hasNonStoreyChildren = spatialNode.children?.some(
184
- (c: SpatialNode) => getNodeType(c.type) !== 'IfcBuildingStorey'
209
+ (c: SpatialNode) => !isStoreyLikeSpatialType(c.type)
185
210
  );
186
211
  const hasChildren = stopAtBuilding
187
- ? (nodeType !== 'IfcBuilding' && hasNonStoreyChildren)
188
- : (spatialNode.children?.length > 0) ||
189
- ((nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace') && elements.length > 0);
212
+ ? Boolean(hasNonStoreyChildren || hasDirectElements)
213
+ : (spatialNode.children?.length > 0) || hasDirectElements;
190
214
 
191
215
  nodes.push({
192
216
  id: nodeId,
@@ -201,7 +225,7 @@ function buildSpatialNodes(
201
225
  hasChildren,
202
226
  isExpanded: isNodeExpanded,
203
227
  isVisible: true, // Visibility computed lazily during render
204
- elementCount: nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace' ? elements.length : undefined,
228
+ elementCount: hasDirectElements ? elements.length : undefined,
205
229
  storeyElevation: spatialNode.elevation,
206
230
  // Store idOffset for lazy visibility computation
207
231
  _idOffset: idOffset,
@@ -209,7 +233,8 @@ function buildSpatialNodes(
209
233
 
210
234
  if (isNodeExpanded) {
211
235
  // Sort storeys by elevation descending
212
- const sortedChildren = nodeType === 'IfcBuilding'
236
+ const shouldSortByElevation = (spatialNode.children || []).some((child) => isStoreyLikeSpatialType(child.type));
237
+ const sortedChildren = shouldSortByElevation
213
238
  ? [...(spatialNode.children || [])].sort((a, b) => (b.elevation || 0) - (a.elevation || 0))
214
239
  : spatialNode.children || [];
215
240
 
@@ -229,8 +254,8 @@ function buildSpatialNodes(
229
254
  );
230
255
  }
231
256
 
232
- // For storeys (single-model only), add elements
233
- if (!stopAtBuilding && (nodeType === 'IfcBuildingStorey' || nodeType === 'IfcSpace') && elements.length > 0) {
257
+ // Add direct spatial children elements for expanded nodes.
258
+ if (hasDirectElements) {
234
259
  for (const elementId of elements) {
235
260
  const globalId = resolveTreeGlobalId(modelId, elementId, models);
236
261
  const entityType = dataStore.entities?.getTypeName(elementId) || 'Unknown';
@@ -9,7 +9,16 @@ export type NodeType =
9
9
  | 'IfcProject' // Project node
10
10
  | 'IfcSite' // Site node
11
11
  | 'IfcBuilding' // Building node
12
+ | 'IfcFacility' // IFC4.3 facility root
13
+ | 'IfcBridge' // IFC4.3 bridge root
14
+ | 'IfcRoad' // IFC4.3 road root
15
+ | 'IfcRailway' // IFC4.3 railway root
16
+ | 'IfcMarineFacility' // IFC4.3 marine facility root
12
17
  | 'IfcBuildingStorey' // Storey node
18
+ | 'IfcFacilityPart' // IFC4.3 facility part
19
+ | 'IfcBridgePart' // IFC4.3 bridge part
20
+ | 'IfcRoadPart' // IFC4.3 road part
21
+ | 'IfcRailwayPart' // IFC4.3 railway part
13
22
  | 'IfcSpace' // Space node
14
23
  | 'type-group' // IFC class grouping header (e.g., "IfcWall (47)")
15
24
  | 'ifc-type' // IFC type entity node (e.g., "IfcWallType/W01")
@@ -57,6 +66,19 @@ export interface UnifiedStorey {
57
66
  totalElements: number;
58
67
  }
59
68
 
60
- // Spatial container types (Project/Site/Building) - these don't have direct visibility toggles
61
- const SPATIAL_CONTAINER_TYPES: Set<NodeType> = new Set(['IfcProject', 'IfcSite', 'IfcBuilding']);
69
+ // Spatial container types (all non-leaf spatial nodes) - these don't participate in storey filters.
70
+ const SPATIAL_CONTAINER_TYPES: Set<NodeType> = new Set([
71
+ 'IfcProject',
72
+ 'IfcSite',
73
+ 'IfcBuilding',
74
+ 'IfcFacility',
75
+ 'IfcBridge',
76
+ 'IfcRoad',
77
+ 'IfcRailway',
78
+ 'IfcMarineFacility',
79
+ 'IfcFacilityPart',
80
+ 'IfcBridgePart',
81
+ 'IfcRoadPart',
82
+ 'IfcRailwayPart',
83
+ ]);
62
84
  export const isSpatialContainer = (type: NodeType): boolean => SPATIAL_CONTAINER_TYPES.has(type);
@@ -5,15 +5,8 @@
5
5
  import type { EntityRef, VisibilityBackendMethods } from '@ifc-lite/sdk';
6
6
  import type { StoreApi } from './types.js';
7
7
  import { getModelForRef, type ModelLike } from './model-compat.js';
8
- import { collectIfcBuildingStoreyElementsWithIfcSpace } from '../../store/basketVisibleSet.js';
9
- import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';
10
-
11
- const SPATIAL_TYPES = new Set([
12
- 'IfcBuildingStorey',
13
- 'IfcBuilding',
14
- 'IfcSite',
15
- 'IfcProject',
16
- ]);
8
+ import { collectSpatialSubtreeElementsWithIfcSpace } from '../../store/basketVisibleSet.js';
9
+ import { isSpaceLikeSpatialTypeName, isSpatialStructureTypeName, type SpatialNode } from '@ifc-lite/data';
17
10
 
18
11
  function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode | null {
19
12
  const stack: SpatialNode[] = [root];
@@ -27,21 +20,6 @@ function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode |
27
20
  return null;
28
21
  }
29
22
 
30
- function collectDescendantStoreyIds(node: SpatialNode): number[] {
31
- const storeyIds: number[] = [];
32
- const stack: SpatialNode[] = [node];
33
- while (stack.length > 0) {
34
- const current = stack.pop()!;
35
- if (current.type === IfcTypeEnum.IfcBuildingStorey) {
36
- storeyIds.push(current.expressId);
37
- }
38
- for (const child of current.children) {
39
- stack.push(child);
40
- }
41
- }
42
- return storeyIds;
43
- }
44
-
45
23
  /**
46
24
  * If `ref` points to a spatial structure element (storey, building, etc.),
47
25
  * expand it to the local expressIds of all contained elements.
@@ -50,37 +28,17 @@ function collectDescendantStoreyIds(node: SpatialNode): number[] {
50
28
  function expandSpatialRef(ref: EntityRef, model: ModelLike): number[] {
51
29
  const dataStore = model.ifcDataStore;
52
30
  const typeName = dataStore.entities.getTypeName(ref.expressId) || '';
53
- if (!SPATIAL_TYPES.has(typeName)) return [ref.expressId];
31
+ if (!isSpatialStructureTypeName(typeName) || isSpaceLikeSpatialTypeName(typeName)) {
32
+ return [ref.expressId];
33
+ }
54
34
 
55
35
  const hierarchy = dataStore.spatialHierarchy;
56
36
  if (!hierarchy) return [ref.expressId];
57
-
58
- if (typeName === 'IfcBuildingStorey') {
59
- const ids = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, ref.expressId);
60
- return ids && ids.length > 0 ? ids : [ref.expressId];
61
- }
62
-
63
- // For higher-level containers (IfcBuilding, IfcSite, IfcProject),
64
- // walk the spatial tree from ref.expressId to find descendant storeys only
65
37
  const startNode = findDescendantNode(hierarchy.project, ref.expressId);
66
38
  if (!startNode) return [ref.expressId];
67
39
 
68
- const descendantStoreyIds = collectDescendantStoreyIds(startNode);
69
-
70
- const allIds: number[] = [];
71
- const seen = new Set<number>();
72
- for (const storeyId of descendantStoreyIds) {
73
- const storeyIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, storeyId);
74
- if (storeyIds) {
75
- for (const id of storeyIds) {
76
- if (!seen.has(id)) {
77
- seen.add(id);
78
- allIds.push(id);
79
- }
80
- }
81
- }
82
- }
83
- return allIds.length > 0 ? allIds : [ref.expressId];
40
+ const ids = collectSpatialSubtreeElementsWithIfcSpace(hierarchy, ref.expressId);
41
+ return ids && ids.length > 0 ? ids : [ref.expressId];
84
42
  }
85
43
 
86
44
  export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMethods {
@@ -2,18 +2,88 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import { describe, it, beforeEach } from 'node:test';
6
- import assert from 'node:assert';
7
- import { useViewerStore } from './index.js';
5
+ import assert from 'node:assert/strict';
6
+ import { beforeEach, describe, it } from 'node:test';
7
+ import { IfcTypeEnum, type SpatialHierarchy, type SpatialNode } from '@ifc-lite/data';
8
8
  import {
9
+ collectSpatialSubtreeElementsWithIfcSpace,
9
10
  getSmartBasketInputFromStore,
10
11
  getBasketSelectionRefsFromStore,
11
12
  getVisibleBasketEntityRefsFromStore,
12
13
  isBasketIsolationActiveFromStore,
13
14
  invalidateVisibleBasketCache,
14
15
  } from './basketVisibleSet.js';
16
+ import { useViewerStore } from './index.js';
15
17
  import { entityRefToString } from './types.js';
16
18
 
19
+ function createNode(expressId: number, type: IfcTypeEnum, children: SpatialNode[] = [], elements: number[] = []): SpatialNode {
20
+ return {
21
+ expressId,
22
+ type,
23
+ name: `Node ${expressId}`,
24
+ children,
25
+ elements,
26
+ };
27
+ }
28
+
29
+ describe('collectSpatialSubtreeElementsWithIfcSpace', () => {
30
+ it('collects direct and descendant IFC4.3 spatial contents for facility-part hierarchies', () => {
31
+ const partNode = createNode(3, IfcTypeEnum.IfcBridgePart, [], [4]);
32
+ const bridgeNode = createNode(2, IfcTypeEnum.IfcBridge, [partNode], []);
33
+ const projectNode = createNode(1, IfcTypeEnum.IfcProject, [bridgeNode], []);
34
+
35
+ const hierarchy: SpatialHierarchy = {
36
+ project: projectNode,
37
+ byStorey: new Map(),
38
+ byBuilding: new Map([[2, []]]),
39
+ bySite: new Map(),
40
+ bySpace: new Map(),
41
+ storeyElevations: new Map(),
42
+ storeyHeights: new Map(),
43
+ elementToStorey: new Map(),
44
+ getStoreyElements: () => [],
45
+ getStoreyByElevation: () => null,
46
+ getContainingSpace: () => null,
47
+ getPath: () => [],
48
+ };
49
+
50
+ assert.deepEqual(collectSpatialSubtreeElementsWithIfcSpace(hierarchy, 2), [4]);
51
+ });
52
+
53
+ it('keeps the selected container when the spatial subtree has no descendant elements', () => {
54
+ const bridgeNode = createNode(2, IfcTypeEnum.IfcBridge, [], []);
55
+ const projectNode = createNode(1, IfcTypeEnum.IfcProject, [bridgeNode], []);
56
+
57
+ const hierarchy: SpatialHierarchy = {
58
+ project: projectNode,
59
+ byStorey: new Map(),
60
+ byBuilding: new Map([[2, []]]),
61
+ bySite: new Map(),
62
+ bySpace: new Map(),
63
+ storeyElevations: new Map(),
64
+ storeyHeights: new Map(),
65
+ elementToStorey: new Map(),
66
+ getStoreyElements: () => [],
67
+ getStoreyByElevation: () => null,
68
+ getContainingSpace: () => null,
69
+ getPath: () => [],
70
+ };
71
+
72
+ useViewerStore.setState({
73
+ ifcDataStore: {
74
+ spatialHierarchy: hierarchy,
75
+ entities: { getTypeName: () => 'IfcBridge' },
76
+ } as any,
77
+ selectedEntity: { modelId: 'legacy', expressId: 2 },
78
+ selectedEntities: [],
79
+ selectedEntityIds: new Set(),
80
+ selectedEntitiesSet: new Set(),
81
+ });
82
+
83
+ assert.deepEqual(getBasketSelectionRefsFromStore(), [{ modelId: 'legacy', expressId: 2 }]);
84
+ });
85
+ });
86
+
17
87
  describe('basketVisibleSet', () => {
18
88
  beforeEach(() => {
19
89
  invalidateVisibleBasketCache();