@ifc-lite/export 1.11.5 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/dist/ifc5-exporter.d.ts +101 -0
- package/dist/ifc5-exporter.d.ts.map +1 -0
- package/dist/ifc5-exporter.js +487 -0
- package/dist/ifc5-exporter.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/merged-exporter.d.ts +2 -2
- package/dist/merged-exporter.d.ts.map +1 -1
- package/dist/merged-exporter.js +16 -4
- package/dist/merged-exporter.js.map +1 -1
- package/dist/schema-converter.d.ts +50 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +380 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/step-exporter.d.ts +2 -2
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +15 -2
- package/dist/step-exporter.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ifc-lite/export
|
|
2
2
|
|
|
3
|
-
Export formats for
|
|
3
|
+
Export formats for IFC-Lite. Supports exporting IFC data to glTF/GLB, Apache Parquet, Apache Arrow, IFC STEP, and IFC5 IFCX.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ npm install @ifc-lite/export
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { GLTFExporter, ParquetExporter, exportToStep } from '@ifc-lite/export';
|
|
14
|
+
import { GLTFExporter, ParquetExporter, exportToStep, Ifc5Exporter } from '@ifc-lite/export';
|
|
15
15
|
|
|
16
16
|
// Export geometry to GLB
|
|
17
17
|
const gltfExporter = new GLTFExporter();
|
|
@@ -22,7 +22,12 @@ const parquetExporter = new ParquetExporter();
|
|
|
22
22
|
const parquet = await parquetExporter.exportEntities(parseResult);
|
|
23
23
|
|
|
24
24
|
// Export to IFC STEP (with mutations applied)
|
|
25
|
-
const step =
|
|
25
|
+
const step = exportToStep(dataStore, { schema: 'IFC4' });
|
|
26
|
+
|
|
27
|
+
// Export to IFC5 IFCX (JSON + USD geometry)
|
|
28
|
+
const ifc5 = new Ifc5Exporter(dataStore, geometryResult);
|
|
29
|
+
const result = ifc5.export({ includeGeometry: true });
|
|
30
|
+
// result.content is IFCX JSON, save as .ifcx
|
|
26
31
|
```
|
|
27
32
|
|
|
28
33
|
## Features
|
|
@@ -31,6 +36,34 @@ const step = await exportToStep(dataStore, mutations);
|
|
|
31
36
|
- Apache Parquet serialization (columnar, compressed)
|
|
32
37
|
- Apache Arrow in-memory format
|
|
33
38
|
- IFC STEP export with mutations applied
|
|
39
|
+
- **IFC5 IFCX export** with USD geometry and full schema conversion
|
|
40
|
+
- Cross-schema conversion (IFC2X3 ↔ IFC4 ↔ IFC4X3 ↔ IFC5)
|
|
41
|
+
|
|
42
|
+
## IFC5 Export
|
|
43
|
+
|
|
44
|
+
The `Ifc5Exporter` converts IFC data from any schema version to the IFC5 IFCX JSON format:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { Ifc5Exporter } from '@ifc-lite/export';
|
|
48
|
+
|
|
49
|
+
const exporter = new Ifc5Exporter(dataStore, geometryResult, mutationView);
|
|
50
|
+
const result = exporter.export({
|
|
51
|
+
includeGeometry: true, // Convert meshes to USD format
|
|
52
|
+
includeProperties: true, // Map properties to bsi::ifc::prop:: namespace
|
|
53
|
+
applyMutations: true, // Apply property edits
|
|
54
|
+
visibleOnly: false, // Filter by visibility
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// result.content → IFCX JSON string
|
|
58
|
+
// result.stats → { nodeCount, propertyCount, meshCount, fileSize }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Output includes:
|
|
62
|
+
- Entity types converted to IFC5 naming
|
|
63
|
+
- Properties in IFCX attribute namespaces (`bsi::ifc::prop::PsetName::PropName`)
|
|
64
|
+
- Tessellated geometry as USD meshes (`usd::usdgeom::mesh`)
|
|
65
|
+
- Spatial hierarchy as IFCX path-based nodes
|
|
66
|
+
- Presentation data (diffuse color, opacity)
|
|
34
67
|
|
|
35
68
|
## API
|
|
36
69
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IFC5 (IFCX) Exporter
|
|
3
|
+
*
|
|
4
|
+
* Converts an IfcDataStore (from IFC2X3/IFC4/IFC4X3 STEP files) to the
|
|
5
|
+
* IFC5 IFCX JSON format with USD geometry.
|
|
6
|
+
*
|
|
7
|
+
* This performs full schema conversion:
|
|
8
|
+
* - Entity type mapping to IFC5 (aligned with IFC4X3 naming)
|
|
9
|
+
* - Properties converted to IFCX attribute namespaces
|
|
10
|
+
* - Tessellated geometry converted to USD mesh format
|
|
11
|
+
* - Spatial hierarchy mapped to IFCX path-based structure
|
|
12
|
+
*/
|
|
13
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
14
|
+
import type { MutablePropertyView } from '@ifc-lite/mutations';
|
|
15
|
+
import type { GeometryResult } from '@ifc-lite/geometry';
|
|
16
|
+
/** Options for IFC5 export */
|
|
17
|
+
export interface Ifc5ExportOptions {
|
|
18
|
+
/** Author name */
|
|
19
|
+
author?: string;
|
|
20
|
+
/** Data version identifier */
|
|
21
|
+
dataVersion?: string;
|
|
22
|
+
/** Include geometry as USD meshes (default: true) */
|
|
23
|
+
includeGeometry?: boolean;
|
|
24
|
+
/** Include properties (default: true) */
|
|
25
|
+
includeProperties?: boolean;
|
|
26
|
+
/** Apply mutations (default: true if mutation view provided) */
|
|
27
|
+
applyMutations?: boolean;
|
|
28
|
+
/** Pretty print JSON (default: true) */
|
|
29
|
+
prettyPrint?: boolean;
|
|
30
|
+
/** Only export visible entities */
|
|
31
|
+
visibleOnly?: boolean;
|
|
32
|
+
/** Hidden entity IDs (local expressIds) */
|
|
33
|
+
hiddenEntityIds?: Set<number>;
|
|
34
|
+
/** Isolated entity IDs (local expressIds, null = no isolation) */
|
|
35
|
+
isolatedEntityIds?: Set<number> | null;
|
|
36
|
+
}
|
|
37
|
+
/** Result of IFC5 export */
|
|
38
|
+
export interface Ifc5ExportResult {
|
|
39
|
+
/** IFCX JSON content */
|
|
40
|
+
content: string;
|
|
41
|
+
/** Statistics */
|
|
42
|
+
stats: {
|
|
43
|
+
nodeCount: number;
|
|
44
|
+
propertyCount: number;
|
|
45
|
+
meshCount: number;
|
|
46
|
+
fileSize: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Exports IFC data (from any schema) to IFC5 IFCX JSON format.
|
|
51
|
+
*/
|
|
52
|
+
export declare class Ifc5Exporter {
|
|
53
|
+
private dataStore;
|
|
54
|
+
private mutationView;
|
|
55
|
+
private geometryResult;
|
|
56
|
+
private idOffset;
|
|
57
|
+
/** Unique path segment name per entity (with _<id> suffix when siblings collide) */
|
|
58
|
+
private segmentNames;
|
|
59
|
+
/** Real names from SpatialNode tree (reliable for spatial containers) */
|
|
60
|
+
private spatialNodeNames;
|
|
61
|
+
/** Spatial container children (Project→Sites, Site→Buildings, etc.) */
|
|
62
|
+
private spatialChildIds;
|
|
63
|
+
constructor(dataStore: IfcDataStore, geometryResult?: GeometryResult | null, mutationView?: MutablePropertyView, idOffset?: number);
|
|
64
|
+
/**
|
|
65
|
+
* Export to IFCX JSON format
|
|
66
|
+
*/
|
|
67
|
+
export(options?: Ifc5ExportOptions): Ifc5ExportResult;
|
|
68
|
+
/**
|
|
69
|
+
* Build path strings for all entities using the spatial hierarchy.
|
|
70
|
+
* Paths follow IFCX convention: "0/SiteName/BuildingName/StoreyName/ElementName"
|
|
71
|
+
*/
|
|
72
|
+
private buildEntityPaths;
|
|
73
|
+
/**
|
|
74
|
+
* Get properties for an entity, converted to IFCX attribute format.
|
|
75
|
+
*/
|
|
76
|
+
private getPropertiesForEntity;
|
|
77
|
+
/**
|
|
78
|
+
* Convert a property value to IFCX-compatible format.
|
|
79
|
+
* IFCX uses native JSON types rather than IFC wrapped types.
|
|
80
|
+
*/
|
|
81
|
+
private convertPropertyValue;
|
|
82
|
+
/**
|
|
83
|
+
* Get children for a spatial entity.
|
|
84
|
+
* IFCX children format: { childName: childPath }
|
|
85
|
+
*/
|
|
86
|
+
private getChildrenForEntity;
|
|
87
|
+
/**
|
|
88
|
+
* Build mesh lookup from GeometryResult, keyed by original expressId.
|
|
89
|
+
*/
|
|
90
|
+
private buildMeshLookup;
|
|
91
|
+
/**
|
|
92
|
+
* Convert tessellated MeshData (Y-up) to USD mesh format (Z-up).
|
|
93
|
+
* Merges multiple mesh fragments for the same entity.
|
|
94
|
+
*/
|
|
95
|
+
private convertToUsdMesh;
|
|
96
|
+
/**
|
|
97
|
+
* Build visible entity set if visibility filtering is requested.
|
|
98
|
+
*/
|
|
99
|
+
private buildVisibleSet;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=ifc5-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ifc5-exporter.d.ts","sourceRoot":"","sources":["../src/ifc5-exporter.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAY,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAanE,8BAA8B;AAC9B,MAAM,WAAW,iBAAiB;IAChC,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gEAAgE;IAChE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wCAAwC;IACxC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mCAAmC;IACnC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2CAA2C;IAC3C,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CACxC;AAED,4BAA4B;AAC5B,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AA4BD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAS;IACzB,oFAAoF;IACpF,OAAO,CAAC,YAAY,CAA6B;IACjD,yEAAyE;IACzE,OAAO,CAAC,gBAAgB,CAA6B;IACrD,uEAAuE;IACvE,OAAO,CAAC,eAAe,CAA+B;gBAGpD,SAAS,EAAE,YAAY,EACvB,cAAc,CAAC,EAAE,cAAc,GAAG,IAAI,EACtC,YAAY,CAAC,EAAE,mBAAmB,EAClC,QAAQ,CAAC,EAAE,MAAM;IAQnB;;OAEG;IACH,MAAM,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB;IA4IzD;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoJxB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA4B9B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA6C5B;;OAEG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAqExB;;OAEG;IACH,OAAO,CAAC,eAAe;CAsBxB"}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { IfcTypeEnumToString, IfcTypeEnumFromString, PropertyValueType, } from '@ifc-lite/data';
|
|
5
|
+
import { convertEntityType } from './schema-converter.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Exporter
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Exports IFC data (from any schema) to IFC5 IFCX JSON format.
|
|
11
|
+
*/
|
|
12
|
+
export class Ifc5Exporter {
|
|
13
|
+
dataStore;
|
|
14
|
+
mutationView;
|
|
15
|
+
geometryResult;
|
|
16
|
+
idOffset;
|
|
17
|
+
/** Unique path segment name per entity (with _<id> suffix when siblings collide) */
|
|
18
|
+
segmentNames = new Map();
|
|
19
|
+
/** Real names from SpatialNode tree (reliable for spatial containers) */
|
|
20
|
+
spatialNodeNames = new Map();
|
|
21
|
+
/** Spatial container children (Project→Sites, Site→Buildings, etc.) */
|
|
22
|
+
spatialChildIds = new Map();
|
|
23
|
+
constructor(dataStore, geometryResult, mutationView, idOffset) {
|
|
24
|
+
this.dataStore = dataStore;
|
|
25
|
+
this.geometryResult = geometryResult ?? null;
|
|
26
|
+
this.mutationView = mutationView ?? null;
|
|
27
|
+
this.idOffset = idOffset ?? 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Export to IFCX JSON format
|
|
31
|
+
*/
|
|
32
|
+
export(options = {}) {
|
|
33
|
+
const sourceSchema = this.dataStore.schemaVersion || 'IFC4';
|
|
34
|
+
// Build entity path map using spatial hierarchy
|
|
35
|
+
const entityPaths = this.buildEntityPaths();
|
|
36
|
+
// Build mesh lookup by expressId
|
|
37
|
+
const meshByEntity = this.buildMeshLookup(options);
|
|
38
|
+
// Build visible set
|
|
39
|
+
const visibleIds = this.buildVisibleSet(options);
|
|
40
|
+
// Collect nodes
|
|
41
|
+
const nodes = [];
|
|
42
|
+
let propertyCount = 0;
|
|
43
|
+
let meshCount = 0;
|
|
44
|
+
const { entities, strings } = this.dataStore;
|
|
45
|
+
for (let i = 0; i < entities.count; i++) {
|
|
46
|
+
const expressId = entities.expressId[i];
|
|
47
|
+
// Visibility filter
|
|
48
|
+
if (visibleIds && !visibleIds.has(expressId))
|
|
49
|
+
continue;
|
|
50
|
+
const typeEnum = entities.typeEnum[i];
|
|
51
|
+
const typeName = IfcTypeEnumToString(typeEnum) || 'IfcElement';
|
|
52
|
+
// Convert entity type to IFC5 (aligned with IFC4X3)
|
|
53
|
+
const ifc5Type = convertEntityType(typeName.toUpperCase(), sourceSchema, 'IFC5');
|
|
54
|
+
// Convert back to PascalCase for IFCX
|
|
55
|
+
const ifc5Class = stepTypeToClassName(ifc5Type);
|
|
56
|
+
// Get path for this entity
|
|
57
|
+
const path = entityPaths.get(expressId) || `ifc:${ifc5Class}.${expressId}`;
|
|
58
|
+
// Build attributes
|
|
59
|
+
const attributes = {};
|
|
60
|
+
// IFC class
|
|
61
|
+
attributes['bsi::ifc::class'] = { code: ifc5Class };
|
|
62
|
+
// GlobalId
|
|
63
|
+
const globalId = strings.get(entities.globalId[i]);
|
|
64
|
+
if (globalId) {
|
|
65
|
+
attributes['bsi::ifc::globalId'] = globalId;
|
|
66
|
+
}
|
|
67
|
+
// Name - only write when real data exists (don't fabricate names)
|
|
68
|
+
const name = strings.get(entities.name[i])
|
|
69
|
+
|| this.spatialNodeNames.get(expressId);
|
|
70
|
+
if (name) {
|
|
71
|
+
attributes['bsi::ifc::name'] = name;
|
|
72
|
+
}
|
|
73
|
+
// Description
|
|
74
|
+
const description = strings.get(entities.description[i]);
|
|
75
|
+
if (description) {
|
|
76
|
+
attributes['bsi::ifc::description'] = description;
|
|
77
|
+
}
|
|
78
|
+
// Properties
|
|
79
|
+
if (options.includeProperties !== false) {
|
|
80
|
+
const props = this.getPropertiesForEntity(expressId, options);
|
|
81
|
+
for (const [key, value] of Object.entries(props)) {
|
|
82
|
+
attributes[key] = value;
|
|
83
|
+
propertyCount++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Build node
|
|
87
|
+
const node = { path };
|
|
88
|
+
// Children from spatial hierarchy
|
|
89
|
+
const children = this.getChildrenForEntity(expressId, entityPaths);
|
|
90
|
+
if (Object.keys(children).length > 0) {
|
|
91
|
+
node.children = children;
|
|
92
|
+
}
|
|
93
|
+
// Geometry as USD mesh
|
|
94
|
+
if (options.includeGeometry !== false) {
|
|
95
|
+
const meshes = meshByEntity.get(expressId);
|
|
96
|
+
if (meshes && meshes.length > 0) {
|
|
97
|
+
const usdMesh = this.convertToUsdMesh(meshes);
|
|
98
|
+
attributes['usd::usdgeom::mesh'] = usdMesh;
|
|
99
|
+
// Color/presentation
|
|
100
|
+
const [r, g, b, a] = meshes[0].color;
|
|
101
|
+
attributes['bsi::ifc::presentation::diffuseColor'] = [r, g, b];
|
|
102
|
+
if (a < 1.0) {
|
|
103
|
+
attributes['bsi::ifc::presentation::opacity'] = a;
|
|
104
|
+
}
|
|
105
|
+
meshCount++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(attributes).length > 0) {
|
|
109
|
+
node.attributes = attributes;
|
|
110
|
+
}
|
|
111
|
+
nodes.push(node);
|
|
112
|
+
}
|
|
113
|
+
// Assemble IFCX file
|
|
114
|
+
const file = {
|
|
115
|
+
header: {
|
|
116
|
+
id: `ifcx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
117
|
+
ifcxVersion: 'IFCX-1.0',
|
|
118
|
+
dataVersion: options.dataVersion || '1.0.0',
|
|
119
|
+
author: options.author || 'ifc-lite',
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
},
|
|
122
|
+
imports: [],
|
|
123
|
+
schemas: {},
|
|
124
|
+
data: nodes,
|
|
125
|
+
};
|
|
126
|
+
const content = options.prettyPrint !== false
|
|
127
|
+
? JSON.stringify(file, null, 2)
|
|
128
|
+
: JSON.stringify(file);
|
|
129
|
+
return {
|
|
130
|
+
content,
|
|
131
|
+
stats: {
|
|
132
|
+
nodeCount: nodes.length,
|
|
133
|
+
propertyCount,
|
|
134
|
+
meshCount,
|
|
135
|
+
fileSize: new TextEncoder().encode(content).length,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// --------------------------------------------------------------------------
|
|
140
|
+
// Path building
|
|
141
|
+
// --------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Build path strings for all entities using the spatial hierarchy.
|
|
144
|
+
* Paths follow IFCX convention: "0/SiteName/BuildingName/StoreyName/ElementName"
|
|
145
|
+
*/
|
|
146
|
+
buildEntityPaths() {
|
|
147
|
+
const paths = new Map();
|
|
148
|
+
const { spatialHierarchy, entities, strings } = this.dataStore;
|
|
149
|
+
if (!spatialHierarchy)
|
|
150
|
+
return paths;
|
|
151
|
+
// Build parent→children map from hierarchy
|
|
152
|
+
const parentOf = new Map();
|
|
153
|
+
const processChildren = (parentId, childIds) => {
|
|
154
|
+
if (!childIds)
|
|
155
|
+
return;
|
|
156
|
+
for (const childId of childIds) {
|
|
157
|
+
parentOf.set(childId, parentId);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// Add spatial container hierarchy from the project tree first
|
|
161
|
+
// (Project→Site, Site→Building, Building→Storey, Storey→Space)
|
|
162
|
+
// Also collect spatial node names (SpatialNode.name is often more reliable
|
|
163
|
+
// than the entity table for spatial containers)
|
|
164
|
+
this.spatialChildIds.clear();
|
|
165
|
+
this.spatialNodeNames.clear();
|
|
166
|
+
if (spatialHierarchy.project) {
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
const walkTree = (node) => {
|
|
169
|
+
if (node.name) {
|
|
170
|
+
this.spatialNodeNames.set(node.expressId, node.name);
|
|
171
|
+
}
|
|
172
|
+
const childIds = [];
|
|
173
|
+
for (const child of node.children) {
|
|
174
|
+
parentOf.set(child.expressId, node.expressId);
|
|
175
|
+
childIds.push(child.expressId);
|
|
176
|
+
walkTree(child);
|
|
177
|
+
}
|
|
178
|
+
this.spatialChildIds.set(node.expressId, childIds);
|
|
179
|
+
};
|
|
180
|
+
walkTree(spatialHierarchy.project);
|
|
181
|
+
}
|
|
182
|
+
// Add element containment from flat maps (element→storey/building/site/space)
|
|
183
|
+
if (spatialHierarchy.bySite) {
|
|
184
|
+
for (const [siteId, children] of spatialHierarchy.bySite) {
|
|
185
|
+
processChildren(siteId, children);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (spatialHierarchy.byBuilding) {
|
|
189
|
+
for (const [buildingId, children] of spatialHierarchy.byBuilding) {
|
|
190
|
+
processChildren(buildingId, children);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (spatialHierarchy.byStorey) {
|
|
194
|
+
for (const [storeyId, children] of spatialHierarchy.byStorey) {
|
|
195
|
+
processChildren(storeyId, children);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (spatialHierarchy.bySpace) {
|
|
199
|
+
for (const [spaceId, children] of spatialHierarchy.bySpace) {
|
|
200
|
+
processChildren(spaceId, children);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Build index lookup for entity names.
|
|
204
|
+
// Priority: entity table name → spatial node name → IFC type name fallback
|
|
205
|
+
const entityNameById = new Map();
|
|
206
|
+
for (let i = 0; i < entities.count; i++) {
|
|
207
|
+
const id = entities.expressId[i];
|
|
208
|
+
let name = strings.get(entities.name[i]) || '';
|
|
209
|
+
// For entities with empty names, try spatial node name (from hierarchy tree)
|
|
210
|
+
if (!name) {
|
|
211
|
+
name = this.spatialNodeNames.get(id) || '';
|
|
212
|
+
}
|
|
213
|
+
// Last resort: use the IFC type name so paths are readable (e.g. "IfcProject")
|
|
214
|
+
if (!name) {
|
|
215
|
+
const typeName = IfcTypeEnumToString(entities.typeEnum[i]);
|
|
216
|
+
if (typeName !== 'Unknown') {
|
|
217
|
+
name = typeName;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
entityNameById.set(id, name);
|
|
221
|
+
}
|
|
222
|
+
// Build children-per-parent map so we can detect name collisions among siblings
|
|
223
|
+
const childrenOf = new Map();
|
|
224
|
+
for (const [childId, parentId] of parentOf) {
|
|
225
|
+
if (!childrenOf.has(parentId))
|
|
226
|
+
childrenOf.set(parentId, []);
|
|
227
|
+
childrenOf.get(parentId).push(childId);
|
|
228
|
+
}
|
|
229
|
+
// Also collect root entities (those not in parentOf)
|
|
230
|
+
for (let i = 0; i < entities.count; i++) {
|
|
231
|
+
const id = entities.expressId[i];
|
|
232
|
+
if (!parentOf.has(id)) {
|
|
233
|
+
if (!childrenOf.has(undefined))
|
|
234
|
+
childrenOf.set(undefined, []);
|
|
235
|
+
childrenOf.get(undefined).push(id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Pre-compute unique segment names: append _<expressId> when siblings share a name
|
|
239
|
+
this.segmentNames.clear();
|
|
240
|
+
for (const [, siblings] of childrenOf) {
|
|
241
|
+
// Count how many siblings share each sanitised name
|
|
242
|
+
const nameCount = new Map();
|
|
243
|
+
for (const id of siblings) {
|
|
244
|
+
const raw = entityNameById.get(id) || `e${id}`;
|
|
245
|
+
const safe = raw.replace(/[/\\]/g, '_').replace(/\s+/g, '_');
|
|
246
|
+
nameCount.set(safe, (nameCount.get(safe) || 0) + 1);
|
|
247
|
+
}
|
|
248
|
+
for (const id of siblings) {
|
|
249
|
+
const raw = entityNameById.get(id) || `e${id}`;
|
|
250
|
+
const safe = raw.replace(/[/\\]/g, '_').replace(/\s+/g, '_');
|
|
251
|
+
this.segmentNames.set(id, nameCount.get(safe) > 1 ? `${safe}_${id}` : safe);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Generate path for each entity by walking up to root
|
|
255
|
+
const getPath = (expressId) => {
|
|
256
|
+
if (paths.has(expressId))
|
|
257
|
+
return paths.get(expressId);
|
|
258
|
+
const segments = [];
|
|
259
|
+
let current = expressId;
|
|
260
|
+
const visited = new Set();
|
|
261
|
+
while (current !== undefined) {
|
|
262
|
+
if (visited.has(current))
|
|
263
|
+
break; // cycle protection
|
|
264
|
+
visited.add(current);
|
|
265
|
+
segments.unshift(this.segmentNames.get(current) || `e${current}`);
|
|
266
|
+
const parent = parentOf.get(current);
|
|
267
|
+
if (parent === undefined)
|
|
268
|
+
break;
|
|
269
|
+
current = parent;
|
|
270
|
+
}
|
|
271
|
+
// Prefix with "0" (root index, per IFCX convention)
|
|
272
|
+
const path = '0/' + segments.join('/');
|
|
273
|
+
paths.set(expressId, path);
|
|
274
|
+
return path;
|
|
275
|
+
};
|
|
276
|
+
for (let i = 0; i < entities.count; i++) {
|
|
277
|
+
getPath(entities.expressId[i]);
|
|
278
|
+
}
|
|
279
|
+
return paths;
|
|
280
|
+
}
|
|
281
|
+
// --------------------------------------------------------------------------
|
|
282
|
+
// Properties
|
|
283
|
+
// --------------------------------------------------------------------------
|
|
284
|
+
/**
|
|
285
|
+
* Get properties for an entity, converted to IFCX attribute format.
|
|
286
|
+
*/
|
|
287
|
+
getPropertiesForEntity(entityId, options) {
|
|
288
|
+
const result = {};
|
|
289
|
+
// Prefer mutation view if available
|
|
290
|
+
if (this.mutationView && options.applyMutations !== false) {
|
|
291
|
+
const psets = this.mutationView.getForEntity(entityId);
|
|
292
|
+
for (const pset of psets) {
|
|
293
|
+
for (const prop of pset.properties) {
|
|
294
|
+
const key = `bsi::ifc::prop::${pset.name}::${prop.name}`;
|
|
295
|
+
result[key] = this.convertPropertyValue(prop.value, prop.type);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (this.dataStore.properties) {
|
|
300
|
+
const psets = this.dataStore.properties.getForEntity(entityId);
|
|
301
|
+
for (const pset of psets) {
|
|
302
|
+
for (const prop of pset.properties) {
|
|
303
|
+
const key = `bsi::ifc::prop::${pset.name}::${prop.name}`;
|
|
304
|
+
result[key] = this.convertPropertyValue(prop.value, prop.type);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Convert a property value to IFCX-compatible format.
|
|
312
|
+
* IFCX uses native JSON types rather than IFC wrapped types.
|
|
313
|
+
*/
|
|
314
|
+
convertPropertyValue(value, type) {
|
|
315
|
+
if (value === null || value === undefined)
|
|
316
|
+
return null;
|
|
317
|
+
switch (type) {
|
|
318
|
+
case PropertyValueType.Real:
|
|
319
|
+
return Number(value);
|
|
320
|
+
case PropertyValueType.Integer:
|
|
321
|
+
return Math.round(Number(value));
|
|
322
|
+
case PropertyValueType.Boolean:
|
|
323
|
+
case PropertyValueType.Logical:
|
|
324
|
+
return Boolean(value);
|
|
325
|
+
default:
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// --------------------------------------------------------------------------
|
|
330
|
+
// Children
|
|
331
|
+
// --------------------------------------------------------------------------
|
|
332
|
+
/**
|
|
333
|
+
* Get children for a spatial entity.
|
|
334
|
+
* IFCX children format: { childName: childPath }
|
|
335
|
+
*/
|
|
336
|
+
getChildrenForEntity(entityId, entityPaths) {
|
|
337
|
+
const children = {};
|
|
338
|
+
const addChild = (childId) => {
|
|
339
|
+
const childPath = entityPaths.get(childId);
|
|
340
|
+
if (!childPath)
|
|
341
|
+
return;
|
|
342
|
+
const childName = this.segmentNames.get(childId) || `e${childId}`;
|
|
343
|
+
children[childName] = childPath;
|
|
344
|
+
};
|
|
345
|
+
// Spatial container children (Project→Sites, Site→Buildings, etc.)
|
|
346
|
+
const spatialKids = this.spatialChildIds.get(entityId);
|
|
347
|
+
if (spatialKids) {
|
|
348
|
+
for (const childId of spatialKids) {
|
|
349
|
+
addChild(childId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Element children from containment maps
|
|
353
|
+
const { spatialHierarchy } = this.dataStore;
|
|
354
|
+
if (spatialHierarchy) {
|
|
355
|
+
const childSets = [
|
|
356
|
+
spatialHierarchy.bySite?.get(entityId),
|
|
357
|
+
spatialHierarchy.byBuilding?.get(entityId),
|
|
358
|
+
spatialHierarchy.byStorey?.get(entityId),
|
|
359
|
+
spatialHierarchy.bySpace?.get(entityId),
|
|
360
|
+
];
|
|
361
|
+
for (const childSet of childSets) {
|
|
362
|
+
if (!childSet)
|
|
363
|
+
continue;
|
|
364
|
+
for (const childId of childSet) {
|
|
365
|
+
addChild(childId);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return children;
|
|
370
|
+
}
|
|
371
|
+
// --------------------------------------------------------------------------
|
|
372
|
+
// Geometry conversion
|
|
373
|
+
// --------------------------------------------------------------------------
|
|
374
|
+
/**
|
|
375
|
+
* Build mesh lookup from GeometryResult, keyed by original expressId.
|
|
376
|
+
*/
|
|
377
|
+
buildMeshLookup(options) {
|
|
378
|
+
const lookup = new Map();
|
|
379
|
+
if (!this.geometryResult || options.includeGeometry === false)
|
|
380
|
+
return lookup;
|
|
381
|
+
for (const mesh of this.geometryResult.meshes) {
|
|
382
|
+
// Convert global expressId back to original local expressId
|
|
383
|
+
const localId = mesh.expressId - this.idOffset;
|
|
384
|
+
const id = localId > 0 ? localId : mesh.expressId;
|
|
385
|
+
if (!lookup.has(id)) {
|
|
386
|
+
lookup.set(id, []);
|
|
387
|
+
}
|
|
388
|
+
lookup.get(id).push(mesh);
|
|
389
|
+
}
|
|
390
|
+
return lookup;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Convert tessellated MeshData (Y-up) to USD mesh format (Z-up).
|
|
394
|
+
* Merges multiple mesh fragments for the same entity.
|
|
395
|
+
*/
|
|
396
|
+
convertToUsdMesh(meshes) {
|
|
397
|
+
const allPoints = [];
|
|
398
|
+
const allIndices = [];
|
|
399
|
+
const allFaceCounts = [];
|
|
400
|
+
const allNormals = [];
|
|
401
|
+
let indexOffset = 0;
|
|
402
|
+
for (const mesh of meshes) {
|
|
403
|
+
// Convert positions from Y-up to Z-up
|
|
404
|
+
// Y-up: X=right, Y=up, Z=back
|
|
405
|
+
// Z-up: X=right, Y=forward, Z=up
|
|
406
|
+
// Reverse of: Yx=Zx, Yy=Zz, Yz=-Zy
|
|
407
|
+
for (let i = 0; i < mesh.positions.length; i += 3) {
|
|
408
|
+
const x = mesh.positions[i];
|
|
409
|
+
const y = mesh.positions[i + 1]; // Y-up Y = Z-up Z
|
|
410
|
+
const z = mesh.positions[i + 2]; // Y-up Z = -Z-up Y
|
|
411
|
+
allPoints.push([x, -z, y]);
|
|
412
|
+
}
|
|
413
|
+
// Convert normals from Y-up to Z-up
|
|
414
|
+
if (mesh.normals) {
|
|
415
|
+
for (let i = 0; i < mesh.normals.length; i += 3) {
|
|
416
|
+
const nx = mesh.normals[i];
|
|
417
|
+
const ny = mesh.normals[i + 1];
|
|
418
|
+
const nz = mesh.normals[i + 2];
|
|
419
|
+
allNormals.push([nx, -nz, ny]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Offset indices for merged mesh
|
|
423
|
+
for (let i = 0; i < mesh.indices.length; i += 3) {
|
|
424
|
+
allIndices.push(mesh.indices[i] + indexOffset, mesh.indices[i + 1] + indexOffset, mesh.indices[i + 2] + indexOffset);
|
|
425
|
+
allFaceCounts.push(3); // triangles
|
|
426
|
+
}
|
|
427
|
+
indexOffset += mesh.positions.length / 3;
|
|
428
|
+
}
|
|
429
|
+
const result = {
|
|
430
|
+
points: allPoints,
|
|
431
|
+
faceVertexIndices: allIndices,
|
|
432
|
+
faceVertexCounts: allFaceCounts,
|
|
433
|
+
};
|
|
434
|
+
if (allNormals.length > 0) {
|
|
435
|
+
result.normals = allNormals;
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
// --------------------------------------------------------------------------
|
|
440
|
+
// Visibility
|
|
441
|
+
// --------------------------------------------------------------------------
|
|
442
|
+
/**
|
|
443
|
+
* Build visible entity set if visibility filtering is requested.
|
|
444
|
+
*/
|
|
445
|
+
buildVisibleSet(options) {
|
|
446
|
+
if (!options.visibleOnly)
|
|
447
|
+
return null;
|
|
448
|
+
const hidden = options.hiddenEntityIds ?? new Set();
|
|
449
|
+
const isolated = options.isolatedEntityIds ?? null;
|
|
450
|
+
const visible = new Set();
|
|
451
|
+
const { entities } = this.dataStore;
|
|
452
|
+
for (let i = 0; i < entities.count; i++) {
|
|
453
|
+
const id = entities.expressId[i];
|
|
454
|
+
if (isolated) {
|
|
455
|
+
// When isolation is active, only isolated entities are visible
|
|
456
|
+
if (isolated.has(id))
|
|
457
|
+
visible.add(id);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Otherwise, everything except hidden is visible
|
|
461
|
+
if (!hidden.has(id))
|
|
462
|
+
visible.add(id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return visible;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// Helpers
|
|
470
|
+
// ============================================================================
|
|
471
|
+
/**
|
|
472
|
+
* Convert STEP uppercase type name (e.g. "IFCWALL") to PascalCase class name (e.g. "IfcWall").
|
|
473
|
+
* Uses the IFC type enum lookup for canonical casing (e.g. "IfcRelAggregates", not "Ifcrelaggregates").
|
|
474
|
+
*/
|
|
475
|
+
function stepTypeToClassName(stepType) {
|
|
476
|
+
const enumVal = IfcTypeEnumFromString(stepType);
|
|
477
|
+
const name = IfcTypeEnumToString(enumVal);
|
|
478
|
+
if (name !== 'Unknown')
|
|
479
|
+
return name;
|
|
480
|
+
// Fallback for types not in the enum: simple prefix normalisation
|
|
481
|
+
const lower = stepType.toLowerCase();
|
|
482
|
+
if (lower.startsWith('ifc')) {
|
|
483
|
+
return 'Ifc' + lower.charAt(3).toUpperCase() + lower.slice(4);
|
|
484
|
+
}
|
|
485
|
+
return stepType;
|
|
486
|
+
}
|
|
487
|
+
//# sourceMappingURL=ifc5-exporter.js.map
|