@ifc-lite/export 1.11.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ifc-lite/export
2
2
 
3
- Export formats for IFClite. Supports exporting IFC data to glTF/GLB, Apache Parquet, Apache Arrow, and IFC.
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 = await exportToStep(dataStore, mutations);
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