@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.
- package/.turbo/turbo-build.log +11 -15
- package/.turbo/turbo-typecheck.log +41 -1
- package/CHANGELOG.md +10 -0
- package/dist/assets/{Arrow.dom-CcoDLP6E.js → Arrow.dom-DuPUrOxJ.js} +1 -1
- package/dist/assets/{basketViewActivator-FtbS__bG.js → basketViewActivator-DetjPnvt.js} +1 -1
- package/dist/assets/{browser-CXd3z0DO.js → browser-BQdwnOUt.js} +1 -1
- package/dist/assets/geometry.worker-Bjm-ukng.js +1 -0
- package/dist/assets/ifc-lite_bg-DD0A7Yow.wasm +0 -0
- package/dist/assets/{index-DqNiuQep.js → index-B3X21yXA.js} +4 -4
- package/dist/assets/{index-D99fzcwI.js → index-BybGZJTW.js} +13456 -13364
- package/dist/assets/{native-bridge-DjDj2M6p.js → native-bridge-CN0ZMR2t.js} +1 -1
- package/dist/assets/{wasm-bridge-CDTF4ZQc.js → wasm-bridge-D0bALkma.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +11 -11
- package/src/components/viewer/PropertiesPanel.tsx +6 -7
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +70 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +35 -10
- package/src/components/viewer/hierarchy/types.ts +24 -2
- package/src/sdk/adapters/visibility-adapter.ts +7 -49
- package/src/store/basketVisibleSet.test.ts +73 -3
- package/src/store/basketVisibleSet.ts +46 -75
- package/src/utils/serverDataModel.test.ts +90 -0
- package/src/utils/serverDataModel.ts +22 -34
- package/src/utils/spatialHierarchy.test.ts +38 -0
- package/src/utils/spatialHierarchy.ts +13 -23
- package/dist/assets/ifc-lite-TI3u_Zyw.js +0 -7
- package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
- package/dist/assets/workerHelpers-G7llXNMi.js +0 -36
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./index-
|
|
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-
|
|
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.
|
|
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/
|
|
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.
|
|
55
|
-
"@ifc-lite/
|
|
56
|
-
"@ifc-lite/
|
|
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/
|
|
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,
|
|
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
|
|
587
|
-
|
|
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
|
|
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 {
|
|
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 (
|
|
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) =>
|
|
209
|
+
(c: SpatialNode) => !isStoreyLikeSpatialType(c.type)
|
|
185
210
|
);
|
|
186
211
|
const hasChildren = stopAtBuilding
|
|
187
|
-
? (
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
233
|
-
if (
|
|
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 (
|
|
61
|
-
const SPATIAL_CONTAINER_TYPES: Set<NodeType> = new Set([
|
|
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 {
|
|
9
|
-
import {
|
|
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 (!
|
|
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
|
|
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
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
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();
|