@ifc-lite/export 1.17.2 → 1.18.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/README.md +60 -61
- package/dist/ifc5-exporter.d.ts.map +1 -1
- package/dist/ifc5-exporter.js +0 -2
- package/dist/ifc5-exporter.js.map +1 -1
- package/dist/merged-exporter.d.ts.map +1 -1
- package/dist/merged-exporter.js +29 -30
- package/dist/merged-exporter.js.map +1 -1
- package/dist/parquet-exporter.d.ts.map +1 -1
- package/dist/parquet-exporter.js +6 -7
- package/dist/parquet-exporter.js.map +1 -1
- package/dist/step-exporter.d.ts +14 -23
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +192 -264
- package/dist/step-exporter.js.map +1 -1
- package/dist/step-serialization.d.ts +61 -0
- package/dist/step-serialization.d.ts.map +1 -0
- package/dist/step-serialization.js +253 -0
- package/dist/step-serialization.js.map +1 -0
- package/package.json +11 -11
package/dist/step-exporter.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
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
|
import { EntityExtractor, generateHeader, getAttributeNames, serializeValue, ref, } from '@ifc-lite/parser';
|
|
5
|
-
import {
|
|
5
|
+
import { safeUtf8Decode } from '@ifc-lite/data';
|
|
6
6
|
import { generateIfcGuid } from '@ifc-lite/encoding';
|
|
7
7
|
import { collectReferencedEntityIds, getVisibleEntityIds, collectStyleEntities } from './reference-collector.js';
|
|
8
8
|
import { convertStepLine, needsConversion } from './schema-converter.js';
|
|
9
|
+
import { escapeStepString, toStepReal, quantityTypeToIfcType, serializePropertyValue, serializeAttributeValue, serializeStepArgs, serializeStepValue, splitTopLevelArgs, splitTopLevelStepArguments, } from './step-serialization.js';
|
|
9
10
|
/**
|
|
10
11
|
* IFC STEP file exporter
|
|
11
12
|
*/
|
|
@@ -17,8 +18,11 @@ export class StepExporter {
|
|
|
17
18
|
constructor(dataStore, mutationView) {
|
|
18
19
|
this.dataStore = dataStore;
|
|
19
20
|
this.mutationView = mutationView || null;
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const maxExisting = this.findMaxExpressId();
|
|
22
|
+
const overlayWatermark = typeof mutationView?.peekNextExpressId === 'function'
|
|
23
|
+
? mutationView.peekNextExpressId() - 1
|
|
24
|
+
: 0;
|
|
25
|
+
this.nextExpressId = Math.max(maxExisting, overlayWatermark) + 1;
|
|
22
26
|
this.entityExtractor = dataStore.source ? new EntityExtractor(dataStore.source) : null;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
@@ -85,6 +89,12 @@ export class StepExporter {
|
|
|
85
89
|
}
|
|
86
90
|
targetMap.get(mutation.entityId).add(mutation.psetName);
|
|
87
91
|
}
|
|
92
|
+
// Build a reverse index of IfcRelDefinesByProperties → (relId, psetId)
|
|
93
|
+
// pairs keyed on each related entity. The two property/quantity loops
|
|
94
|
+
// below previously walked every entity in `entityIndex.byId` per
|
|
95
|
+
// modified entity (O(E·N)); the index keeps the per-entity step
|
|
96
|
+
// O(K) where K is the number of rels referencing that entity.
|
|
97
|
+
const relDefinesByEntity = this.buildRelDefinesByPropertiesIndex();
|
|
88
98
|
// Collect modified property sets and find original psets to skip
|
|
89
99
|
for (const [entityId, psetNames] of entityPropMutations) {
|
|
90
100
|
modifiedEntities.add(entityId);
|
|
@@ -97,28 +107,23 @@ export class StepExporter {
|
|
|
97
107
|
if (relevantPsets.length > 0) {
|
|
98
108
|
newPropertySets.push({ entityId, psets: relevantPsets });
|
|
99
109
|
}
|
|
100
|
-
// Find original property set IDs and relationship IDs to skip
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
skipPropertySetIds.add(
|
|
117
|
-
// Also skip the individual properties in this pset
|
|
118
|
-
const propIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
119
|
-
for (const propId of propIds) {
|
|
120
|
-
skipPropertySetIds.add(propId);
|
|
121
|
-
}
|
|
110
|
+
// Find original property set IDs and relationship IDs to skip — look
|
|
111
|
+
// up only the IfcRelDefinesByProperties rels that reference this entity.
|
|
112
|
+
const rels = relDefinesByEntity.get(entityId);
|
|
113
|
+
if (rels) {
|
|
114
|
+
for (const { relId, psetId: relatedPsetId } of rels) {
|
|
115
|
+
// Check if this pset is one we're modifying
|
|
116
|
+
const psetName = this.getPropertySetName(relatedPsetId);
|
|
117
|
+
if (psetName) {
|
|
118
|
+
relDefinedPsetNames.add(psetName);
|
|
119
|
+
}
|
|
120
|
+
if (psetName && psetNames.has(psetName)) {
|
|
121
|
+
skipRelationshipIds.add(relId);
|
|
122
|
+
skipPropertySetIds.add(relatedPsetId);
|
|
123
|
+
// Also skip the individual properties in this pset
|
|
124
|
+
const propIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
125
|
+
for (const propId of propIds) {
|
|
126
|
+
skipPropertySetIds.add(propId);
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
}
|
|
@@ -161,21 +166,18 @@ export class StepExporter {
|
|
|
161
166
|
if (relevantQsets.length > 0) {
|
|
162
167
|
newQuantitySets.push({ entityId, qsets: relevantQsets });
|
|
163
168
|
}
|
|
164
|
-
// Skip original quantity set entities (IfcElementQuantity)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const quantId of quantIds) {
|
|
177
|
-
skipPropertySetIds.add(quantId);
|
|
178
|
-
}
|
|
169
|
+
// Skip original quantity set entities (IfcElementQuantity).
|
|
170
|
+
// Same per-entity index lookup as the property branch above.
|
|
171
|
+
const rels = relDefinesByEntity.get(entityId);
|
|
172
|
+
if (rels) {
|
|
173
|
+
for (const { relId, psetId: relatedPsetId } of rels) {
|
|
174
|
+
const qsetName = this.getElementQuantityName(relatedPsetId);
|
|
175
|
+
if (qsetName && qsetNames.has(qsetName)) {
|
|
176
|
+
skipRelationshipIds.add(relId);
|
|
177
|
+
skipPropertySetIds.add(relatedPsetId);
|
|
178
|
+
const quantIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
179
|
+
for (const quantId of quantIds) {
|
|
180
|
+
skipPropertySetIds.add(quantId);
|
|
179
181
|
}
|
|
180
182
|
}
|
|
181
183
|
}
|
|
@@ -279,12 +281,12 @@ export class StepExporter {
|
|
|
279
281
|
const crs = gm.projectedCRS;
|
|
280
282
|
const crsId = this.nextExpressId++;
|
|
281
283
|
// IfcProjectedCRS(Name, Description, GeodeticDatum, VerticalDatum, MapProjection, MapZone, MapUnit)
|
|
282
|
-
const name = crs.name ? `'${
|
|
283
|
-
const desc = crs.description ? `'${
|
|
284
|
-
const datum = crs.geodeticDatum ? `'${
|
|
285
|
-
const vDatum = crs.verticalDatum ? `'${
|
|
286
|
-
const proj = crs.mapProjection ? `'${
|
|
287
|
-
const zone = crs.mapZone ? `'${
|
|
284
|
+
const name = crs.name ? `'${escapeStepString(String(crs.name))}'` : '$';
|
|
285
|
+
const desc = crs.description ? `'${escapeStepString(String(crs.description))}'` : '$';
|
|
286
|
+
const datum = crs.geodeticDatum ? `'${escapeStepString(String(crs.geodeticDatum))}'` : '$';
|
|
287
|
+
const vDatum = crs.verticalDatum ? `'${escapeStepString(String(crs.verticalDatum))}'` : '$';
|
|
288
|
+
const proj = crs.mapProjection ? `'${escapeStepString(String(crs.mapProjection))}'` : '$';
|
|
289
|
+
const zone = crs.mapZone ? `'${escapeStepString(String(crs.mapZone))}'` : '$';
|
|
288
290
|
const mapUnitRef = crs.mapUnit
|
|
289
291
|
? `#${this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines)}`
|
|
290
292
|
: '$';
|
|
@@ -295,12 +297,12 @@ export class StepExporter {
|
|
|
295
297
|
if (contextId) {
|
|
296
298
|
const mc = gm.mapConversion || {};
|
|
297
299
|
const mcId = this.nextExpressId++;
|
|
298
|
-
const eastings =
|
|
299
|
-
const northings =
|
|
300
|
-
const height =
|
|
301
|
-
const abscissa = mc.xAxisAbscissa !== undefined ?
|
|
302
|
-
const ordinate = mc.xAxisOrdinate !== undefined ?
|
|
303
|
-
const scale = mc.scale !== undefined ?
|
|
300
|
+
const eastings = toStepReal(Number(mc.eastings) || 0);
|
|
301
|
+
const northings = toStepReal(Number(mc.northings) || 0);
|
|
302
|
+
const height = toStepReal(Number(mc.orthogonalHeight) || 0);
|
|
303
|
+
const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
|
|
304
|
+
const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
|
|
305
|
+
const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
|
|
304
306
|
// IfcMapConversion(SourceCRS, TargetCRS, Eastings, Northings, OrthogonalHeight, XAxisAbscissa, XAxisOrdinate, Scale)
|
|
305
307
|
newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${crsId},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
|
|
306
308
|
newEntityCount++;
|
|
@@ -315,12 +317,12 @@ export class StepExporter {
|
|
|
315
317
|
if (contextId) {
|
|
316
318
|
const mc = gm.mapConversion;
|
|
317
319
|
const mcId = this.nextExpressId++;
|
|
318
|
-
const eastings =
|
|
319
|
-
const northings =
|
|
320
|
-
const height =
|
|
321
|
-
const abscissa = mc.xAxisAbscissa !== undefined ?
|
|
322
|
-
const ordinate = mc.xAxisOrdinate !== undefined ?
|
|
323
|
-
const scale = mc.scale !== undefined ?
|
|
320
|
+
const eastings = toStepReal(Number(mc.eastings) || 0);
|
|
321
|
+
const northings = toStepReal(Number(mc.northings) || 0);
|
|
322
|
+
const height = toStepReal(Number(mc.orthogonalHeight) || 0);
|
|
323
|
+
const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
|
|
324
|
+
const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
|
|
325
|
+
const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
|
|
324
326
|
newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${existingCrsIds[0]},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
|
|
325
327
|
newEntityCount++;
|
|
326
328
|
}
|
|
@@ -329,8 +331,18 @@ export class StepExporter {
|
|
|
329
331
|
}
|
|
330
332
|
}
|
|
331
333
|
}
|
|
332
|
-
// If delta only, only export modified entities
|
|
333
|
-
|
|
334
|
+
// If delta only, only export modified entities. Overlay-created entities
|
|
335
|
+
// also count — without this, `createEntity()`-only edits would silently
|
|
336
|
+
// drop out of delta exports.
|
|
337
|
+
const overlayNewEntityCount = (this.mutationView
|
|
338
|
+
&& options.applyMutations !== false
|
|
339
|
+
&& typeof this.mutationView.getNewEntities === 'function') ? this.mutationView.getNewEntities().length : 0;
|
|
340
|
+
// Georef-only deltas (newGeorefLines populated but no entity changes) must
|
|
341
|
+
// still produce a non-empty DATA section.
|
|
342
|
+
if (options.deltaOnly
|
|
343
|
+
&& modifiedEntities.size === 0
|
|
344
|
+
&& overlayNewEntityCount === 0
|
|
345
|
+
&& newGeorefLines.length === 0) {
|
|
334
346
|
const emptyContent = new TextEncoder().encode(header + 'DATA;\nENDSEC;\nEND-ISO-10303-21;\n');
|
|
335
347
|
return {
|
|
336
348
|
content: emptyContent,
|
|
@@ -354,10 +366,18 @@ export class StepExporter {
|
|
|
354
366
|
}
|
|
355
367
|
// Export original entities from source buffer, SKIPPING modified property sets
|
|
356
368
|
if (!options.deltaOnly && this.dataStore.source) {
|
|
357
|
-
const decoder = new TextDecoder();
|
|
358
369
|
const source = this.dataStore.source;
|
|
359
370
|
// Extract existing entities from source
|
|
371
|
+
const overlayActive = !!this.mutationView && (options.applyMutations !== false);
|
|
360
372
|
for (const [expressId, entityRef] of this.dataStore.entityIndex.byId) {
|
|
373
|
+
// Skip entities deleted via the overlay (only when mutations are applied)
|
|
374
|
+
if (overlayActive && typeof this.mutationView.isDeleted === 'function' && this.mutationView.isDeleted(expressId)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
// Skip overlay-only entities — emitted by the new-entities pass below
|
|
378
|
+
if (entityRef.byteLength === 0 || entityRef.byteOffset < 0) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
361
381
|
// Skip entities outside the visible closure
|
|
362
382
|
if (allowedEntityIds !== null && !allowedEntityIds.has(expressId)) {
|
|
363
383
|
continue;
|
|
@@ -376,11 +396,24 @@ export class StepExporter {
|
|
|
376
396
|
if (options.includeGeometry === false && this.isGeometryEntity(entityType)) {
|
|
377
397
|
continue;
|
|
378
398
|
}
|
|
379
|
-
// Get original entity text
|
|
380
|
-
|
|
381
|
-
|
|
399
|
+
// Get original entity text — safeUtf8Decode handles SAB-backed
|
|
400
|
+
// sources (Firefox/Chrome reject `TextDecoder.decode()` on a
|
|
401
|
+
// SharedArrayBuffer-backed view; the parser deliberately keeps
|
|
402
|
+
// `source` zero-copy SAB-backed for worker sharing).
|
|
403
|
+
const entityText = safeUtf8Decode(source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
404
|
+
let nextEntityText = modifiedAttributes.has(expressId)
|
|
382
405
|
? this.applyAttributeMutations(entityText, entityType, modifiedAttributes.get(expressId))
|
|
383
406
|
: entityText;
|
|
407
|
+
const positional = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
408
|
+
? this.mutationView.getPositionalMutationsForEntity(expressId)
|
|
409
|
+
: null;
|
|
410
|
+
if (positional && positional.size > 0) {
|
|
411
|
+
nextEntityText = this.applyPositionalMutations(nextEntityText, positional);
|
|
412
|
+
if (!modifiedEntities.has(expressId)) {
|
|
413
|
+
modifiedEntities.add(expressId);
|
|
414
|
+
modifiedEntityCount++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
384
417
|
// Apply schema conversion if exporting to a different schema version
|
|
385
418
|
if (converting) {
|
|
386
419
|
const converted = convertStepLine(nextEntityText, sourceSchema, schema);
|
|
@@ -429,6 +462,39 @@ export class StepExporter {
|
|
|
429
462
|
for (const line of newGeorefLines) {
|
|
430
463
|
entities.push(line);
|
|
431
464
|
}
|
|
465
|
+
// Add overlay-created entities (store.addEntity / mutationView.createEntity).
|
|
466
|
+
// Apply the same filters as the source-iteration pass so newly-created
|
|
467
|
+
// beams/slabs don't smuggle their geometry helpers (IfcCartesianPoint,
|
|
468
|
+
// IfcExtrudedAreaSolid, etc.) past `includeGeometry:false` /
|
|
469
|
+
// `exportPropertiesOnly()` modes.
|
|
470
|
+
if (this.mutationView
|
|
471
|
+
&& (options.applyMutations !== false)
|
|
472
|
+
&& typeof this.mutationView.getNewEntities === 'function') {
|
|
473
|
+
for (const entity of this.mutationView.getNewEntities()) {
|
|
474
|
+
// STEP requires UPPERCASE entity type tokens. `NewEntity.type` is
|
|
475
|
+
// stored in canonical PascalCase per the public API contract; the
|
|
476
|
+
// upper-case happens here at the file-format boundary.
|
|
477
|
+
const upperType = entity.type.toUpperCase();
|
|
478
|
+
if (options.includeGeometry === false && this.isGeometryEntity(upperType)) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (allowedEntityIds !== null && !allowedEntityIds.has(entity.expressId)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const line = `#${entity.expressId}=${upperType}(${serializeStepArgs(entity.attributes)});`;
|
|
485
|
+
if (converting) {
|
|
486
|
+
const converted = convertStepLine(line, sourceSchema, schema);
|
|
487
|
+
if (converted !== null) {
|
|
488
|
+
entities.push(converted);
|
|
489
|
+
newEntityCount++;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
entities.push(line);
|
|
494
|
+
newEntityCount++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
432
498
|
// Assemble final file as Uint8Array chunks to avoid V8 string length limit
|
|
433
499
|
const content = assembleStepBytes(header, entities);
|
|
434
500
|
return {
|
|
@@ -486,11 +552,11 @@ export class StepExporter {
|
|
|
486
552
|
for (const prop of pset.properties) {
|
|
487
553
|
const propId = this.nextExpressId++;
|
|
488
554
|
count++;
|
|
489
|
-
const valueStr =
|
|
555
|
+
const valueStr = serializePropertyValue(prop.value, prop.type);
|
|
490
556
|
const unitId = prop.unit ? this.findUnitId(prop.unit) : null;
|
|
491
557
|
const unitStr = unitId !== null ? ref(unitId) : null;
|
|
492
558
|
// #ID=IFCPROPERTYSINGLEVALUE('Name',$,Value,Unit);
|
|
493
|
-
const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${
|
|
559
|
+
const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${escapeStepString(prop.name)}',$,${valueStr},${unitStr ? serializeValue(unitStr) : '$'});`;
|
|
494
560
|
lines.push(line);
|
|
495
561
|
propertyIds.push(propId);
|
|
496
562
|
}
|
|
@@ -500,7 +566,7 @@ export class StepExporter {
|
|
|
500
566
|
const propRefs = propertyIds.map(id => `#${id}`).join(',');
|
|
501
567
|
const globalId = this.generateGlobalId();
|
|
502
568
|
// #ID=IFCPROPERTYSET('GlobalId',$,'Name',$,(#props));
|
|
503
|
-
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',$,'${
|
|
569
|
+
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',$,'${escapeStepString(pset.name)}',$,(${propRefs}));`;
|
|
504
570
|
lines.push(psetLine);
|
|
505
571
|
if (typeOwnedPsetNames?.has(pset.name)) {
|
|
506
572
|
generatedTypeOwnedPsetIds.set(pset.name, psetId);
|
|
@@ -528,10 +594,10 @@ export class StepExporter {
|
|
|
528
594
|
for (const q of qset.quantities) {
|
|
529
595
|
const qId = this.nextExpressId++;
|
|
530
596
|
count++;
|
|
531
|
-
const ifcType =
|
|
597
|
+
const ifcType = quantityTypeToIfcType(q.type);
|
|
532
598
|
// #ID=IFCQUANTITYLENGTH('Name',$,$,Value,$);
|
|
533
|
-
const val =
|
|
534
|
-
const line = `#${qId}=${ifcType}('${
|
|
599
|
+
const val = toStepReal(q.value);
|
|
600
|
+
const line = `#${qId}=${ifcType}('${escapeStepString(q.name)}',$,$,${val},$);`;
|
|
535
601
|
lines.push(line);
|
|
536
602
|
quantityIds.push(qId);
|
|
537
603
|
}
|
|
@@ -541,7 +607,7 @@ export class StepExporter {
|
|
|
541
607
|
const quantRefs = quantityIds.map(id => `#${id}`).join(',');
|
|
542
608
|
const globalId = this.generateGlobalId();
|
|
543
609
|
// #ID=IFCELEMENTQUANTITY('GlobalId',$,'Name',$,$,(#quants));
|
|
544
|
-
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',$,'${
|
|
610
|
+
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',$,'${escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
|
|
545
611
|
lines.push(qsetLine);
|
|
546
612
|
// Create IfcRelDefinesByProperties to link qset to entity
|
|
547
613
|
const relId = this.nextExpressId++;
|
|
@@ -552,60 +618,6 @@ export class StepExporter {
|
|
|
552
618
|
}
|
|
553
619
|
return { lines, count };
|
|
554
620
|
}
|
|
555
|
-
/**
|
|
556
|
-
* Map QuantityType to IFC STEP entity type
|
|
557
|
-
*/
|
|
558
|
-
quantityTypeToIfcType(type) {
|
|
559
|
-
switch (type) {
|
|
560
|
-
case QuantityType.Length: return 'IFCQUANTITYLENGTH';
|
|
561
|
-
case QuantityType.Area: return 'IFCQUANTITYAREA';
|
|
562
|
-
case QuantityType.Volume: return 'IFCQUANTITYVOLUME';
|
|
563
|
-
case QuantityType.Count: return 'IFCQUANTITYCOUNT';
|
|
564
|
-
case QuantityType.Weight: return 'IFCQUANTITYWEIGHT';
|
|
565
|
-
case QuantityType.Time: return 'IFCQUANTITYTIME';
|
|
566
|
-
default: return 'IFCQUANTITYCOUNT';
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Serialize a property value to STEP format
|
|
571
|
-
*/
|
|
572
|
-
serializePropertyValue(value, type) {
|
|
573
|
-
if (value === null || value === undefined) {
|
|
574
|
-
return '$';
|
|
575
|
-
}
|
|
576
|
-
switch (type) {
|
|
577
|
-
case PropertyValueType.String:
|
|
578
|
-
case PropertyValueType.Label:
|
|
579
|
-
case PropertyValueType.Text:
|
|
580
|
-
return `IFCLABEL('${this.escapeStepString(String(value))}')`;
|
|
581
|
-
case PropertyValueType.Identifier:
|
|
582
|
-
return `IFCIDENTIFIER('${this.escapeStepString(String(value))}')`;
|
|
583
|
-
case PropertyValueType.Real:
|
|
584
|
-
const num = Number(value);
|
|
585
|
-
if (!Number.isFinite(num))
|
|
586
|
-
return '$';
|
|
587
|
-
return `IFCREAL(${num.toString().includes('.') ? num : num + '.'})`;
|
|
588
|
-
case PropertyValueType.Integer:
|
|
589
|
-
return `IFCINTEGER(${Math.round(Number(value))})`;
|
|
590
|
-
case PropertyValueType.Boolean:
|
|
591
|
-
case PropertyValueType.Logical:
|
|
592
|
-
if (value === true)
|
|
593
|
-
return `IFCBOOLEAN(.T.)`;
|
|
594
|
-
if (value === false)
|
|
595
|
-
return `IFCBOOLEAN(.F.)`;
|
|
596
|
-
return `IFCLOGICAL(.U.)`;
|
|
597
|
-
case PropertyValueType.Enum:
|
|
598
|
-
return `.${String(value).toUpperCase()}.`;
|
|
599
|
-
case PropertyValueType.List:
|
|
600
|
-
if (Array.isArray(value)) {
|
|
601
|
-
const items = value.map(v => this.serializePropertyValue(v, PropertyValueType.String));
|
|
602
|
-
return `(${items.join(',')})`;
|
|
603
|
-
}
|
|
604
|
-
return '$';
|
|
605
|
-
default:
|
|
606
|
-
return `IFCLABEL('${this.escapeStepString(String(value))}')`;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
621
|
/**
|
|
610
622
|
* Rewrite root IFC attributes directly on the original STEP entity line.
|
|
611
623
|
*/
|
|
@@ -619,13 +631,13 @@ export class StepExporter {
|
|
|
619
631
|
if (attrNames.length === 0) {
|
|
620
632
|
return entityText;
|
|
621
633
|
}
|
|
622
|
-
const args =
|
|
634
|
+
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
623
635
|
let changed = false;
|
|
624
636
|
for (const [attrName, value] of attributeMutations) {
|
|
625
637
|
const index = attrNames.indexOf(attrName);
|
|
626
638
|
if (index < 0 || index >= args.length)
|
|
627
639
|
continue;
|
|
628
|
-
args[index] =
|
|
640
|
+
args[index] = serializeAttributeValue(value, args[index]);
|
|
629
641
|
changed = true;
|
|
630
642
|
}
|
|
631
643
|
if (!changed) {
|
|
@@ -633,77 +645,28 @@ export class StepExporter {
|
|
|
633
645
|
}
|
|
634
646
|
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
635
647
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
return '.T.';
|
|
652
|
-
if (normalized === 'false' || normalized === '.f.')
|
|
653
|
-
return '.F.';
|
|
654
|
-
return '.U.';
|
|
655
|
-
}
|
|
656
|
-
if (/^-?\d+(?:\.\d+)?(?:E[+-]?\d+)?$/i.test(trimmed) && /^-?\d/.test(current)) {
|
|
657
|
-
const numberValue = Number(trimmed);
|
|
658
|
-
if (!Number.isFinite(numberValue))
|
|
659
|
-
return '$';
|
|
660
|
-
return current.includes('.') || /E/i.test(current)
|
|
661
|
-
? this.toStepReal(numberValue)
|
|
662
|
-
: String(numberValue);
|
|
663
|
-
}
|
|
664
|
-
return serializeValue(value);
|
|
665
|
-
}
|
|
666
|
-
splitTopLevelArgs(text) {
|
|
667
|
-
const parts = [];
|
|
668
|
-
let current = '';
|
|
669
|
-
let depth = 0;
|
|
670
|
-
let inString = false;
|
|
671
|
-
for (let i = 0; i < text.length; i++) {
|
|
672
|
-
const char = text[i];
|
|
673
|
-
current += char;
|
|
674
|
-
if (inString) {
|
|
675
|
-
if (char === '\'') {
|
|
676
|
-
if (text[i + 1] === '\'') {
|
|
677
|
-
current += text[i + 1];
|
|
678
|
-
i++;
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
inString = false;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (char === '\'') {
|
|
687
|
-
inString = true;
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
if (char === '(') {
|
|
691
|
-
depth++;
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
694
|
-
if (char === ')') {
|
|
695
|
-
depth--;
|
|
648
|
+
/**
|
|
649
|
+
* Apply positional STEP argument overrides to an entity line.
|
|
650
|
+
* Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
|
|
651
|
+
* have no symbolic names. Indexes that fall outside the existing arg list
|
|
652
|
+
* are silently ignored.
|
|
653
|
+
*/
|
|
654
|
+
applyPositionalMutations(entityText, positionals) {
|
|
655
|
+
const openParen = entityText.indexOf('(');
|
|
656
|
+
const closeParen = entityText.lastIndexOf(');');
|
|
657
|
+
if (openParen < 0 || closeParen < openParen)
|
|
658
|
+
return entityText;
|
|
659
|
+
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
660
|
+
let changed = false;
|
|
661
|
+
for (const [index, value] of positionals) {
|
|
662
|
+
if (index < 0 || index >= args.length)
|
|
696
663
|
continue;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
parts.push(current.slice(0, -1).trim());
|
|
700
|
-
current = '';
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (current.trim() || text.endsWith(',')) {
|
|
704
|
-
parts.push(current.trim());
|
|
664
|
+
args[index] = serializeStepValue(value);
|
|
665
|
+
changed = true;
|
|
705
666
|
}
|
|
706
|
-
|
|
667
|
+
if (!changed)
|
|
668
|
+
return entityText;
|
|
669
|
+
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
707
670
|
}
|
|
708
671
|
resolveMapUnitReference(unitName, newGeorefLines) {
|
|
709
672
|
const normalized = this.normalizeMapUnitName(unitName);
|
|
@@ -725,7 +688,7 @@ export class StepExporter {
|
|
|
725
688
|
const name = normalized === 'US SURVEY FOOT' ? 'US SURVEY FOOT' : 'FOOT';
|
|
726
689
|
newGeorefLines.push(`#${dimId}=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);`);
|
|
727
690
|
newGeorefLines.push(`#${siUnitId}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);`);
|
|
728
|
-
newGeorefLines.push(`#${measureId}=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(${
|
|
691
|
+
newGeorefLines.push(`#${measureId}=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(${toStepReal(factor)}),#${siUnitId});`);
|
|
729
692
|
newGeorefLines.push(`#${convUnitId}=IFCCONVERSIONBASEDUNIT(#${dimId},.LENGTHUNIT.,'${name}',#${measureId});`);
|
|
730
693
|
return convUnitId;
|
|
731
694
|
}
|
|
@@ -808,24 +771,6 @@ export class StepExporter {
|
|
|
808
771
|
}
|
|
809
772
|
return first3dContext ?? contextIds[0] ?? null;
|
|
810
773
|
}
|
|
811
|
-
/**
|
|
812
|
-
* Convert a number to a valid STEP REAL literal.
|
|
813
|
-
* Handles NaN/Infinity (→ 0.) and ensures a decimal point is present.
|
|
814
|
-
*/
|
|
815
|
-
toStepReal(v) {
|
|
816
|
-
if (!Number.isFinite(v))
|
|
817
|
-
return '0.';
|
|
818
|
-
const s = v.toString();
|
|
819
|
-
return s.includes('.') ? s : s + '.';
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Escape a string for STEP format
|
|
823
|
-
*/
|
|
824
|
-
escapeStepString(str) {
|
|
825
|
-
return str
|
|
826
|
-
.replace(/\\/g, '\\\\')
|
|
827
|
-
.replace(/'/g, "''");
|
|
828
|
-
}
|
|
829
774
|
/**
|
|
830
775
|
* Generate a new IFC GlobalId (22 character base64)
|
|
831
776
|
*/
|
|
@@ -888,6 +833,31 @@ export class StepExporter {
|
|
|
888
833
|
]);
|
|
889
834
|
return geometryTypes.has(type);
|
|
890
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Build a one-shot reverse index of every IfcRelDefinesByProperties in
|
|
838
|
+
* the source: for each related entity, list the rels and property/quantity
|
|
839
|
+
* sets that reference it. Used by the export pre-pass so the per-entity
|
|
840
|
+
* "find owning rels" step is O(K) rather than O(N) per modified entity.
|
|
841
|
+
*/
|
|
842
|
+
buildRelDefinesByPropertiesIndex() {
|
|
843
|
+
const out = new Map();
|
|
844
|
+
for (const [relId, relRef] of this.dataStore.entityIndex.byId) {
|
|
845
|
+
if (relRef.type.toUpperCase() !== 'IFCRELDEFINESBYPROPERTIES')
|
|
846
|
+
continue;
|
|
847
|
+
const psetId = this.getRelatedPropertySet(relId);
|
|
848
|
+
if (!psetId)
|
|
849
|
+
continue;
|
|
850
|
+
for (const entityId of this.getRelatedEntities(relId)) {
|
|
851
|
+
let bucket = out.get(entityId);
|
|
852
|
+
if (!bucket) {
|
|
853
|
+
bucket = [];
|
|
854
|
+
out.set(entityId, bucket);
|
|
855
|
+
}
|
|
856
|
+
bucket.push({ relId, psetId });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return out;
|
|
860
|
+
}
|
|
891
861
|
/**
|
|
892
862
|
* Get entity IDs related by IfcRelDefinesByProperties (the related objects)
|
|
893
863
|
*/
|
|
@@ -895,8 +865,7 @@ export class StepExporter {
|
|
|
895
865
|
const entityRef = this.dataStore.entityIndex.byId.get(relId);
|
|
896
866
|
if (!entityRef || !this.dataStore.source)
|
|
897
867
|
return [];
|
|
898
|
-
const
|
|
899
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
868
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
900
869
|
// Parse IfcRelDefinesByProperties: #ID=IFCRELDEFINESBYPROPERTIES('guid',$,$,$,(#objects),#pset);
|
|
901
870
|
// The 5th argument (index 4) is the list of related objects
|
|
902
871
|
const match = entityText.match(/\(([^)]+)\)\s*,\s*#(\d+)\s*\)\s*;/);
|
|
@@ -917,8 +886,7 @@ export class StepExporter {
|
|
|
917
886
|
const entityRef = this.dataStore.entityIndex.byId.get(relId);
|
|
918
887
|
if (!entityRef || !this.dataStore.source)
|
|
919
888
|
return null;
|
|
920
|
-
const
|
|
921
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
889
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
922
890
|
// Last #ID before the closing );
|
|
923
891
|
const match = entityText.match(/,\s*#(\d+)\s*\)\s*;$/);
|
|
924
892
|
if (!match)
|
|
@@ -932,8 +900,7 @@ export class StepExporter {
|
|
|
932
900
|
const entityRef = this.dataStore.entityIndex.byId.get(psetId);
|
|
933
901
|
if (!entityRef || !this.dataStore.source)
|
|
934
902
|
return null;
|
|
935
|
-
const
|
|
936
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
903
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
937
904
|
// Parse: IFCPROPERTYSET('guid',$,'Name',$,...) - Name is 3rd argument
|
|
938
905
|
const match = entityText.match(/IFCPROPERTYSET\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
939
906
|
if (!match)
|
|
@@ -947,8 +914,7 @@ export class StepExporter {
|
|
|
947
914
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
948
915
|
if (!entityRef || !this.dataStore.source)
|
|
949
916
|
return null;
|
|
950
|
-
const
|
|
951
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
917
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
952
918
|
// Parse: IFCELEMENTQUANTITY('guid',$,'Name',...) - Name is 3rd argument
|
|
953
919
|
const match = entityText.match(/IFCELEMENTQUANTITY\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
954
920
|
if (!match)
|
|
@@ -962,8 +928,7 @@ export class StepExporter {
|
|
|
962
928
|
const entityRef = this.dataStore.entityIndex.byId.get(psetId);
|
|
963
929
|
if (!entityRef || !this.dataStore.source)
|
|
964
930
|
return [];
|
|
965
|
-
const
|
|
966
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
931
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
967
932
|
// Parse: IFCPROPERTYSET(...,(#prop1,#prop2,...)); - Last argument is properties list
|
|
968
933
|
const match = entityText.match(/\(\s*(#[^)]+)\s*\)\s*\)\s*;$/);
|
|
969
934
|
if (!match)
|
|
@@ -1034,54 +999,17 @@ export class StepExporter {
|
|
|
1034
999
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1035
1000
|
if (!entityRef || !this.dataStore.source)
|
|
1036
1001
|
return null;
|
|
1037
|
-
const
|
|
1038
|
-
const entityText = decoder.decode(this.dataStore.source.subarray(entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength));
|
|
1002
|
+
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1039
1003
|
const match = entityText.match(/^(#\d+\s*=\s*\w+\()([\s\S]*)(\)\s*;)\s*$/);
|
|
1040
1004
|
if (!match)
|
|
1041
1005
|
return null;
|
|
1042
1006
|
const [, prefix, attrsText, suffix] = match;
|
|
1043
|
-
const attrs =
|
|
1007
|
+
const attrs = splitTopLevelStepArguments(attrsText);
|
|
1044
1008
|
if (attrIndex >= attrs.length)
|
|
1045
1009
|
return null;
|
|
1046
1010
|
attrs[attrIndex] = replacement;
|
|
1047
1011
|
return `${prefix}${attrs.join(',')}${suffix}`;
|
|
1048
1012
|
}
|
|
1049
|
-
/**
|
|
1050
|
-
* Split a STEP argument list on top-level commas while preserving nested syntax.
|
|
1051
|
-
*/
|
|
1052
|
-
splitTopLevelStepArguments(input) {
|
|
1053
|
-
const parts = [];
|
|
1054
|
-
let current = '';
|
|
1055
|
-
let depth = 0;
|
|
1056
|
-
let inString = false;
|
|
1057
|
-
for (let i = 0; i < input.length; i++) {
|
|
1058
|
-
const char = input[i];
|
|
1059
|
-
if (char === "'") {
|
|
1060
|
-
current += char;
|
|
1061
|
-
if (inString && i + 1 < input.length && input[i + 1] === "'") {
|
|
1062
|
-
current += input[i + 1];
|
|
1063
|
-
i++;
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
inString = !inString;
|
|
1067
|
-
continue;
|
|
1068
|
-
}
|
|
1069
|
-
if (!inString) {
|
|
1070
|
-
if (char === '(')
|
|
1071
|
-
depth++;
|
|
1072
|
-
else if (char === ')')
|
|
1073
|
-
depth--;
|
|
1074
|
-
else if (char === ',' && depth === 0) {
|
|
1075
|
-
parts.push(current);
|
|
1076
|
-
current = '';
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
current += char;
|
|
1081
|
-
}
|
|
1082
|
-
parts.push(current);
|
|
1083
|
-
return parts;
|
|
1084
|
-
}
|
|
1085
1013
|
}
|
|
1086
1014
|
/**
|
|
1087
1015
|
* Quick export function for simple use cases.
|