@ifc-lite/export 2.8.1 → 2.8.3
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 +13 -0
- package/dist/effective-index.d.ts +16 -1
- package/dist/effective-index.d.ts.map +1 -1
- package/dist/effective-index.js +68 -0
- package/dist/effective-index.js.map +1 -1
- package/dist/glb.d.ts.map +1 -1
- package/dist/glb.js +12 -0
- package/dist/glb.js.map +1 -1
- package/dist/ifc5-exporter.d.ts +29 -3
- package/dist/ifc5-exporter.d.ts.map +1 -1
- package/dist/ifc5-exporter.js +170 -55
- package/dist/ifc5-exporter.js.map +1 -1
- package/dist/lod1-generator.d.ts.map +1 -1
- package/dist/lod1-generator.js +5 -0
- package/dist/lod1-generator.js.map +1 -1
- package/dist/merged-exporter.d.ts.map +1 -1
- package/dist/merged-exporter.js +14 -5
- package/dist/merged-exporter.js.map +1 -1
- package/dist/parquet-exporter.d.ts +28 -1
- package/dist/parquet-exporter.d.ts.map +1 -1
- package/dist/parquet-exporter.js +144 -13
- package/dist/parquet-exporter.js.map +1 -1
- package/dist/reference-collector.d.ts +4 -4
- package/dist/reference-collector.d.ts.map +1 -1
- package/dist/reference-collector.js +64 -18
- package/dist/reference-collector.js.map +1 -1
- package/dist/step-exporter.d.ts +38 -0
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +192 -55
- package/dist/step-exporter.js.map +1 -1
- package/dist/type-owned-psets.d.ts +4 -2
- package/dist/type-owned-psets.d.ts.map +1 -1
- package/dist/type-owned-psets.js +4 -2
- package/dist/type-owned-psets.js.map +1 -1
- package/package.json +7 -7
package/dist/step-exporter.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
-
import { EntityExtractor, generateHeader, parseSourceHeader, getAttributeNamesAcrossSchemas, serializeValue, ref, } from '@ifc-lite/parser';
|
|
5
|
-
import { safeUtf8Decode } from '@ifc-lite/data';
|
|
4
|
+
import { asSourceBytes, EntityExtractor, generateHeader, parseSourceHeader, getAttributeNamesAcrossSchemas, serializeValue, ref, } from '@ifc-lite/parser';
|
|
6
5
|
import { generateIfcGuid } from '@ifc-lite/encoding';
|
|
7
6
|
import { collectReferencedEntityIds, getVisibleEntityIds, collectStyleEntities } from './reference-collector.js';
|
|
8
7
|
import { convertStepLine, needsConversion } from './schema-converter.js';
|
|
@@ -14,8 +13,32 @@ import { escapeStepString, toStepReal, quantityTypeToIfcType, serializePropertyV
|
|
|
14
13
|
import { getRealTypedSlots, serializeEntityArgs, serializeAttributeSlot, isTypedMarker } from './attribute-real-slots.js';
|
|
15
14
|
import { getEnumTypedSlots, getStringTypedSlots, serializeEnumToken, serializeStringSlot, } from './attribute-slot-types.js';
|
|
16
15
|
import { serializeQualifiedSelectSlot } from './select-qualification.js';
|
|
16
|
+
/**
|
|
17
|
+
* UTF-8 decode of `[start, end)` of the source, accepting either the raw bytes
|
|
18
|
+
* or the {@link IfcSourceBytes} accessor (#2183). Replaces the direct
|
|
19
|
+
* `safeUtf8Decode(source, …)` calls this file used to make: `decodeUtf8` is
|
|
20
|
+
* SAB-safe in exactly the same way, and routing through the accessor is what
|
|
21
|
+
* lets `IfcDataStore.source` change shape without touching these eight reads.
|
|
22
|
+
*/
|
|
23
|
+
function decodeRange(src, start, end) {
|
|
24
|
+
return asSourceBytes(src).decodeUtf8(start, end);
|
|
25
|
+
}
|
|
17
26
|
/** `OwnerHistory` is slot 1 on every `IfcRoot` subtype, all schemas. */
|
|
18
27
|
const OWNER_HISTORY_SLOT = 1;
|
|
28
|
+
/**
|
|
29
|
+
* Message for the one refusal `export()` can report, shared by the returned
|
|
30
|
+
* `stats.warnings` entry and the console line so the two cannot drift.
|
|
31
|
+
*/
|
|
32
|
+
const MAP_CONVERSION_WITHOUT_CONTEXT_WARNING = 'Cannot create IfcMapConversion: no IfcGeometricRepresentationContext is available to reference as SourceCRS. The IfcProjectedCRS is unaffected.';
|
|
33
|
+
/**
|
|
34
|
+
* Message for the refusal `export()` reports when a map conversion is
|
|
35
|
+
* requested but there is no IfcProjectedCRS to attach it to — none was
|
|
36
|
+
* requested and none exists in the file — distinct from
|
|
37
|
+
* {@link MAP_CONVERSION_WITHOUT_CONTEXT_WARNING}, which is worded for the
|
|
38
|
+
* case where an IfcProjectedCRS exists (or was written) but no context is
|
|
39
|
+
* available to reference.
|
|
40
|
+
*/
|
|
41
|
+
const MAP_CONVERSION_WITHOUT_CRS_WARNING = 'Cannot create IfcMapConversion: no IfcProjectedCRS was requested and none exists in the file to reference as TargetCRS. Nothing was written.';
|
|
19
42
|
/**
|
|
20
43
|
* IFC STEP file exporter
|
|
21
44
|
*/
|
|
@@ -69,7 +92,9 @@ export class StepExporter {
|
|
|
69
92
|
// `sourceHeader`; fall back to parsing the (always-present) source bytes so
|
|
70
93
|
// cache-restored stores — which don't carry `sourceHeader` — still work.
|
|
71
94
|
const sourceHeader = this.dataStore.sourceHeader
|
|
72
|
-
?? (this.dataStore.source
|
|
95
|
+
?? (this.dataStore.source.byteLength > 0
|
|
96
|
+
? parseSourceHeader(this.dataStore.source)
|
|
97
|
+
: undefined);
|
|
73
98
|
// Preserve the exact FILE_SCHEMA identifier (e.g. IFC4X3_ADD2) only when we
|
|
74
99
|
// are NOT converting schemas; conversion must emit the coarse target token.
|
|
75
100
|
const schemaToken = !converting && sourceHeader?.schemaIdentifiers?.[0]
|
|
@@ -115,6 +140,60 @@ export class StepExporter {
|
|
|
115
140
|
// entity has no source bytes, so the source-iteration pass below never sees
|
|
116
141
|
// it and the new-entities pass at the end owns its line entirely (#2006).
|
|
117
142
|
const isOverlayCreated = (entityId) => effective.isOverlayCreated(entityId);
|
|
143
|
+
// Build visible-only closure if requested. Classification, the closure walk
|
|
144
|
+
// and the style pass all run over the EFFECTIVE index: an overlay-created
|
|
145
|
+
// product becomes a root by the same type rules as a parsed one, the walk
|
|
146
|
+
// follows its authored references into the geometry it alone owns, and a
|
|
147
|
+
// tombstoned entity is simply not there. Run over the source buffer, a
|
|
148
|
+
// created wall could never be a root and nothing referenced it, so
|
|
149
|
+
// `visibleOnly` wrote a file without it and said nothing (#2012).
|
|
150
|
+
//
|
|
151
|
+
// Computed here, ahead of the modification-count passes below, because
|
|
152
|
+
// `hasEmittableHostBytes` needs it: a source-backed host EXCLUDED by
|
|
153
|
+
// `visibleOnly` never gets its line written by the source-iteration pass
|
|
154
|
+
// either, so counting it as "modified" would make the header claim a
|
|
155
|
+
// change the DATA section does not contain (CodeRabbit finding on #2414).
|
|
156
|
+
let allowedEntityIds = null;
|
|
157
|
+
if (options.visibleOnly && this.dataStore.source) {
|
|
158
|
+
const { roots, hiddenProductIds } = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null, effective);
|
|
159
|
+
allowedEntityIds = collectReferencedEntityIds(roots, this.dataStore.source, effective, hiddenProductIds);
|
|
160
|
+
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
161
|
+
// geometry. Styled items reference geometry items but nothing references
|
|
162
|
+
// them back, so the forward closure misses them.
|
|
163
|
+
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId: effective, byType: effective.byType });
|
|
164
|
+
}
|
|
165
|
+
// Will THIS entity's own line ever land in the file? The same byte-range
|
|
166
|
+
// test `willBeEmitted` uses (defined further below) and the source-
|
|
167
|
+
// iteration pass's own skip at `entityRef.byteLength === 0` — a source
|
|
168
|
+
// entity with no bytes (a point-cloud / GLB "entity" from
|
|
169
|
+
// `createSyntheticDataStore`, not an overlay-created one) never gets a
|
|
170
|
+
// defining line written, source-iteration or otherwise, so a pset/attribute
|
|
171
|
+
// edit against it must not count as a modification either: the header
|
|
172
|
+
// would describe a change the file does not contain (out-of-scope finding
|
|
173
|
+
// in #2398). Also excludes a source-backed host the visible-only closure
|
|
174
|
+
// above drops — same reasoning, different reason the line never lands.
|
|
175
|
+
//
|
|
176
|
+
// And, like `willBeEmitted` below, excludes a geometry-classified SOURCE
|
|
177
|
+
// host under `includeGeometry: false`: the source-iteration pass's own
|
|
178
|
+
// `isGeometryEntity` skip (further below) drops that line too, so this
|
|
179
|
+
// predicate must agree or a geometry entity's attribute edit inflates the
|
|
180
|
+
// count over an omitted line (CodeRabbit finding on #2414). Guarded by
|
|
181
|
+
// `!deltaOnly` for the same reason `willBeEmitted` is: under `deltaOnly`
|
|
182
|
+
// the source-iteration pass — and its geometry skip — never runs at all,
|
|
183
|
+
// so a source entity's line is assumed to already exist in the file being
|
|
184
|
+
// patched, geometry or not.
|
|
185
|
+
const isGeometryExcluded = (entityId, recordType) => options.includeGeometry === false
|
|
186
|
+
&& this.isGeometryEntity(effective.effectiveType(entityId, recordType));
|
|
187
|
+
const hasEmittableHostBytes = (entityId) => {
|
|
188
|
+
if (allowedEntityIds !== null && !allowedEntityIds.has(entityId))
|
|
189
|
+
return false;
|
|
190
|
+
const ref = effective.get(entityId);
|
|
191
|
+
if (!ref || ref.byteLength <= 0 || ref.byteOffset < 0)
|
|
192
|
+
return false;
|
|
193
|
+
if (options.deltaOnly !== true && isGeometryExcluded(entityId, ref.type))
|
|
194
|
+
return false;
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
118
197
|
// Collect entities that need to be modified or created
|
|
119
198
|
const modifiedEntities = new Set();
|
|
120
199
|
const modifiedPsets = new Map(); // entityId -> psetNames being modified
|
|
@@ -216,7 +295,7 @@ export class StepExporter {
|
|
|
216
295
|
// `newEntityCount` — as are the pset entities this loop goes on to
|
|
217
296
|
// generate. Only the COUNT is guarded; the entity still records its
|
|
218
297
|
// pset edits and still emits them.
|
|
219
|
-
if (!isOverlayCreated(entityId))
|
|
298
|
+
if (!isOverlayCreated(entityId) && hasEmittableHostBytes(entityId))
|
|
220
299
|
modifiedEntityCount++;
|
|
221
300
|
// Get the FULL mutated property sets for this entity (merged base + mutations)
|
|
222
301
|
const allPsets = this.mutationView.getForEntity(entityId);
|
|
@@ -282,7 +361,7 @@ export class StepExporter {
|
|
|
282
361
|
modifiedEntities.add(entityId);
|
|
283
362
|
// See the property loop above — an overlay-created entity is counted as
|
|
284
363
|
// new, not modified.
|
|
285
|
-
if (!isOverlayCreated(entityId) && !modifiedPsets.has(entityId))
|
|
364
|
+
if (!isOverlayCreated(entityId) && !modifiedPsets.has(entityId) && hasEmittableHostBytes(entityId))
|
|
286
365
|
modifiedEntityCount++;
|
|
287
366
|
const allQsets = this.mutationView.getQuantitiesForEntity(entityId);
|
|
288
367
|
const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
|
|
@@ -313,6 +392,11 @@ export class StepExporter {
|
|
|
313
392
|
// one created-then-renamed wall.
|
|
314
393
|
if (isOverlayCreated(entityId))
|
|
315
394
|
continue;
|
|
395
|
+
// A source entity with no bytes never gets its line rewritten (the
|
|
396
|
+
// source-iteration pass skips it), so an attribute edit against it
|
|
397
|
+
// must not inflate the count either.
|
|
398
|
+
if (!hasEmittableHostBytes(entityId))
|
|
399
|
+
continue;
|
|
316
400
|
if (!entityPropMutations.has(entityId) && !entityQuantMutations.has(entityId)) {
|
|
317
401
|
modifiedEntityCount++;
|
|
318
402
|
}
|
|
@@ -320,10 +404,19 @@ export class StepExporter {
|
|
|
320
404
|
}
|
|
321
405
|
// Process georeferencing mutations (only when applyMutations is enabled)
|
|
322
406
|
const newGeorefLines = [];
|
|
407
|
+
const warnings = [];
|
|
323
408
|
if (options.applyMutations !== false && options.georefMutations) {
|
|
324
409
|
const gm = options.georefMutations;
|
|
325
|
-
|
|
326
|
-
|
|
410
|
+
// `effective.byType`, not the raw index: a source IfcProjectedCRS the
|
|
411
|
+
// session tombstoned is still in `dataStore.entityIndex`, so the modify
|
|
412
|
+
// branch below would queue attribute edits against an id the
|
|
413
|
+
// source-iteration pass then skips — the replacement georeferencing
|
|
414
|
+
// vanishes from the file with no error. `effective.byType` drops
|
|
415
|
+
// tombstones and adds overlay-created records, which the new-entities
|
|
416
|
+
// pass applies `modifiedAttributes` to, so both branches agree on which
|
|
417
|
+
// georeferencing entities exist (#2048).
|
|
418
|
+
const existingCrsIds = effective.byType.get('IFCPROJECTEDCRS');
|
|
419
|
+
const existingMcIds = effective.byType.get('IFCMAPCONVERSION');
|
|
327
420
|
// Modify existing IfcProjectedCRS
|
|
328
421
|
if (gm.projectedCRS && existingCrsIds?.length) {
|
|
329
422
|
const entityId = existingCrsIds[0];
|
|
@@ -358,13 +451,14 @@ export class StepExporter {
|
|
|
358
451
|
changed = true;
|
|
359
452
|
}
|
|
360
453
|
if (crs.mapUnit !== undefined) {
|
|
361
|
-
const mapUnitRef = this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines);
|
|
454
|
+
const mapUnitRef = this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines, effective);
|
|
362
455
|
attrMap.set('MapUnit', `#${mapUnitRef}`);
|
|
363
456
|
changed = true;
|
|
364
457
|
}
|
|
365
458
|
if (changed && !modifiedEntities.has(entityId)) {
|
|
366
459
|
modifiedEntities.add(entityId);
|
|
367
|
-
|
|
460
|
+
if (hasEmittableHostBytes(entityId))
|
|
461
|
+
modifiedEntityCount++;
|
|
368
462
|
}
|
|
369
463
|
}
|
|
370
464
|
// Modify existing IfcMapConversion
|
|
@@ -402,7 +496,8 @@ export class StepExporter {
|
|
|
402
496
|
}
|
|
403
497
|
if (changed && !modifiedEntities.has(entityId)) {
|
|
404
498
|
modifiedEntities.add(entityId);
|
|
405
|
-
|
|
499
|
+
if (hasEmittableHostBytes(entityId))
|
|
500
|
+
modifiedEntityCount++;
|
|
406
501
|
}
|
|
407
502
|
}
|
|
408
503
|
// CREATE new georef entities when file has none
|
|
@@ -417,12 +512,12 @@ export class StepExporter {
|
|
|
417
512
|
const proj = crs.mapProjection ? `'${escapeStepString(String(crs.mapProjection))}'` : '$';
|
|
418
513
|
const zone = crs.mapZone ? `'${escapeStepString(String(crs.mapZone))}'` : '$';
|
|
419
514
|
const mapUnitRef = crs.mapUnit
|
|
420
|
-
? `#${this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines)}`
|
|
515
|
+
? `#${this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines, effective)}`
|
|
421
516
|
: '$';
|
|
422
517
|
newGeorefLines.push(`#${crsId}=IFCPROJECTEDCRS(${name},${desc},${datum},${vDatum},${proj},${zone},${mapUnitRef});`);
|
|
423
518
|
newEntityCount++;
|
|
424
519
|
// Find IfcGeometricRepresentationContext as SourceCRS for MapConversion
|
|
425
|
-
const contextId = this.findPreferredGeometricRepresentationContextId();
|
|
520
|
+
const contextId = this.findPreferredGeometricRepresentationContextId(effective);
|
|
426
521
|
if (contextId) {
|
|
427
522
|
const mc = gm.mapConversion || {};
|
|
428
523
|
const mcId = this.nextExpressId++;
|
|
@@ -437,12 +532,12 @@ export class StepExporter {
|
|
|
437
532
|
newEntityCount++;
|
|
438
533
|
}
|
|
439
534
|
else {
|
|
440
|
-
|
|
535
|
+
this.reportMapConversionRefused(warnings);
|
|
441
536
|
}
|
|
442
537
|
}
|
|
443
538
|
else if (gm.mapConversion && !existingMcIds?.length && existingCrsIds?.length) {
|
|
444
539
|
// CRS exists but no MapConversion — create just the conversion
|
|
445
|
-
const contextId = this.findPreferredGeometricRepresentationContextId();
|
|
540
|
+
const contextId = this.findPreferredGeometricRepresentationContextId(effective);
|
|
446
541
|
if (contextId) {
|
|
447
542
|
const mc = gm.mapConversion;
|
|
448
543
|
const mcId = this.nextExpressId++;
|
|
@@ -456,9 +551,17 @@ export class StepExporter {
|
|
|
456
551
|
newEntityCount++;
|
|
457
552
|
}
|
|
458
553
|
else {
|
|
459
|
-
|
|
554
|
+
this.reportMapConversionRefused(warnings);
|
|
460
555
|
}
|
|
461
556
|
}
|
|
557
|
+
else if (gm.mapConversion && !existingMcIds?.length && !existingCrsIds?.length) {
|
|
558
|
+
// A map conversion was requested, but there is no IfcProjectedCRS to
|
|
559
|
+
// reference as TargetCRS: none was requested (the first branch above
|
|
560
|
+
// didn't fire) and none exists in the file. Both CREATE branches are
|
|
561
|
+
// skipped, so nothing is attempted — report the refusal so the
|
|
562
|
+
// caller isn't left with an empty stats.warnings and no hint (#2105).
|
|
563
|
+
this.reportMapConversionRefusedNoCrs(warnings);
|
|
564
|
+
}
|
|
462
565
|
}
|
|
463
566
|
// If delta only, only export modified entities. Overlay-created entities
|
|
464
567
|
// also count — without this, `createEntity()`-only edits would silently
|
|
@@ -480,25 +583,10 @@ export class StepExporter {
|
|
|
480
583
|
newEntityCount: 0,
|
|
481
584
|
modifiedEntityCount: 0,
|
|
482
585
|
fileSize: emptyContent.byteLength,
|
|
586
|
+
warnings,
|
|
483
587
|
},
|
|
484
588
|
};
|
|
485
589
|
}
|
|
486
|
-
// Build visible-only closure if requested. Classification, the closure walk
|
|
487
|
-
// and the style pass all run over the EFFECTIVE index: an overlay-created
|
|
488
|
-
// product becomes a root by the same type rules as a parsed one, the walk
|
|
489
|
-
// follows its authored references into the geometry it alone owns, and a
|
|
490
|
-
// tombstoned entity is simply not there. Run over the source buffer, a
|
|
491
|
-
// created wall could never be a root and nothing referenced it, so
|
|
492
|
-
// `visibleOnly` wrote a file without it and said nothing (#2012).
|
|
493
|
-
let allowedEntityIds = null;
|
|
494
|
-
if (options.visibleOnly && this.dataStore.source) {
|
|
495
|
-
const { roots, hiddenProductIds } = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null, effective);
|
|
496
|
-
allowedEntityIds = collectReferencedEntityIds(roots, this.dataStore.source, effective, hiddenProductIds);
|
|
497
|
-
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
498
|
-
// geometry. Styled items reference geometry items but nothing references
|
|
499
|
-
// them back, so the forward closure misses them.
|
|
500
|
-
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId: effective, byType: effective.byType });
|
|
501
|
-
}
|
|
502
590
|
/**
|
|
503
591
|
* Will this id have a defining STEP line in the output at all?
|
|
504
592
|
*
|
|
@@ -540,7 +628,21 @@ export class StepExporter {
|
|
|
540
628
|
return false;
|
|
541
629
|
// An overlay-created record carries the placeholder byte range and is
|
|
542
630
|
// written by the new-entities pass; a source record needs real bytes.
|
|
543
|
-
|
|
631
|
+
if (effective.isOverlayCreated(entityId)) {
|
|
632
|
+
// The overlay new-entities pass applies its OWN `isGeometryEntity`
|
|
633
|
+
// filter unconditionally — deltaOnly or not (see the comment at that
|
|
634
|
+
// loop, further below) — so this branch mirrors it without the
|
|
635
|
+
// deltaOnly carve-out the source branch gets.
|
|
636
|
+
return !isGeometryExcluded(entityId, ref.type);
|
|
637
|
+
}
|
|
638
|
+
if (!(ref.byteLength > 0 && ref.byteOffset >= 0))
|
|
639
|
+
return false;
|
|
640
|
+
// Mirrors `hasEmittableHostBytes`: under `deltaOnly` the source-
|
|
641
|
+
// iteration pass — and its geometry skip — never runs, so a source
|
|
642
|
+
// entity's line is assumed to already exist in the file being patched.
|
|
643
|
+
if (options.deltaOnly === true)
|
|
644
|
+
return true;
|
|
645
|
+
return !isGeometryExcluded(entityId, ref.type);
|
|
544
646
|
};
|
|
545
647
|
// A modified pset is replaced wholesale, which skips ALL of its member atoms.
|
|
546
648
|
// But IFC exporters deduplicate identical Pset_*Common atoms (e.g. one
|
|
@@ -577,11 +679,11 @@ export class StepExporter {
|
|
|
577
679
|
if (options.includeGeometry === false && this.isGeometryEntity(entityType)) {
|
|
578
680
|
continue;
|
|
579
681
|
}
|
|
580
|
-
// Get original entity text —
|
|
682
|
+
// Get original entity text — decodeRange handles SAB-backed
|
|
581
683
|
// sources (Firefox/Chrome reject `TextDecoder.decode()` on a
|
|
582
684
|
// SharedArrayBuffer-backed view; the parser deliberately keeps
|
|
583
685
|
// `source` zero-copy SAB-backed for worker sharing).
|
|
584
|
-
const entityText =
|
|
686
|
+
const entityText = decodeRange(source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
585
687
|
let nextEntityText = entityText;
|
|
586
688
|
// Entity retype (reassign class) runs FIRST so attribute mutations
|
|
587
689
|
// below resolve against the TARGET class's attribute names. The
|
|
@@ -638,7 +740,7 @@ export class StepExporter {
|
|
|
638
740
|
// see `willBeEmitted` (#1978, #2030, #2012).
|
|
639
741
|
if (!willBeEmitted(entityId))
|
|
640
742
|
continue;
|
|
641
|
-
const newEntities = this.generatePropertySetEntities(entityId, psets, willBeEmitted, typeOwnedPsetNamesByEntity.get(entityId), options.guidRandom);
|
|
743
|
+
const newEntities = this.generatePropertySetEntities(entityId, psets, willBeEmitted, effective, typeOwnedPsetNamesByEntity.get(entityId), options.guidRandom);
|
|
642
744
|
entities.push(...newEntities.lines);
|
|
643
745
|
newEntityCount += newEntities.count;
|
|
644
746
|
generatedTypeOwnedPsetIds.set(entityId, newEntities.generatedTypeOwnedPsetIds);
|
|
@@ -772,6 +874,7 @@ export class StepExporter {
|
|
|
772
874
|
newEntityCount,
|
|
773
875
|
modifiedEntityCount,
|
|
774
876
|
fileSize: content.byteLength,
|
|
877
|
+
warnings,
|
|
775
878
|
},
|
|
776
879
|
};
|
|
777
880
|
}
|
|
@@ -887,7 +990,7 @@ export class StepExporter {
|
|
|
887
990
|
}
|
|
888
991
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
889
992
|
if (entityRef && this.dataStore.source && entityRef.byteLength > 0) {
|
|
890
|
-
const entityText =
|
|
993
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
891
994
|
// #ID=IFCWALL('GlobalId',#owner,...): GlobalId is a quoted STEP string
|
|
892
995
|
// (doubled '' escapes); OwnerHistory is the ref/`$` right after it.
|
|
893
996
|
const match = entityText.match(/=\s*IFC\w+\s*\(\s*'(?:[^']|'')*'\s*,\s*#(\d+)/i);
|
|
@@ -900,7 +1003,7 @@ export class StepExporter {
|
|
|
900
1003
|
/**
|
|
901
1004
|
* Generate STEP entities for property sets
|
|
902
1005
|
*/
|
|
903
|
-
generatePropertySetEntities(entityId, psets, willBeEmitted, typeOwnedPsetNames, random) {
|
|
1006
|
+
generatePropertySetEntities(entityId, psets, willBeEmitted, effective, typeOwnedPsetNames, random) {
|
|
904
1007
|
const lines = [];
|
|
905
1008
|
let count = 0;
|
|
906
1009
|
const generatedTypeOwnedPsetIds = new Map();
|
|
@@ -911,7 +1014,7 @@ export class StepExporter {
|
|
|
911
1014
|
const propId = this.nextExpressId++;
|
|
912
1015
|
count++;
|
|
913
1016
|
const valueStr = serializePropertyValue(prop.value, prop.type);
|
|
914
|
-
const unitId = prop.unit ? this.findUnitId(prop.unit) : null;
|
|
1017
|
+
const unitId = prop.unit ? this.findUnitId(prop.unit, effective) : null;
|
|
915
1018
|
const unitStr = unitId !== null ? ref(unitId) : null;
|
|
916
1019
|
// #ID=IFCPROPERTYSINGLEVALUE('Name',$,Value,Unit);
|
|
917
1020
|
const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${escapeStepString(prop.name)}',$,${valueStr},${unitStr ? serializeValue(unitStr) : '$'});`;
|
|
@@ -1141,9 +1244,9 @@ export class StepExporter {
|
|
|
1141
1244
|
const forceReal = realSlots.has(index) || tokenIsRealLiteral(currentToken);
|
|
1142
1245
|
return serializeStepValue(value, forceReal);
|
|
1143
1246
|
}
|
|
1144
|
-
resolveMapUnitReference(unitName, newGeorefLines) {
|
|
1247
|
+
resolveMapUnitReference(unitName, newGeorefLines, effective) {
|
|
1145
1248
|
const normalized = this.normalizeMapUnitName(unitName);
|
|
1146
|
-
const existing = this.findLengthUnitReference(normalized);
|
|
1249
|
+
const existing = this.findLengthUnitReference(normalized, effective);
|
|
1147
1250
|
if (existing !== null) {
|
|
1148
1251
|
return existing;
|
|
1149
1252
|
}
|
|
@@ -1179,14 +1282,22 @@ export class StepExporter {
|
|
|
1179
1282
|
return 'FOOT';
|
|
1180
1283
|
return normalized;
|
|
1181
1284
|
}
|
|
1182
|
-
|
|
1285
|
+
/**
|
|
1286
|
+
* `effective` filters the candidates the same way the georef reads above do:
|
|
1287
|
+
* returning a tombstoned unit id hands the caller a `#id` for a line the
|
|
1288
|
+
* export never writes. Returning null instead makes `resolveMapUnitReference`
|
|
1289
|
+
* synthesise a fresh unit, which is the outcome a deleted unit deserves.
|
|
1290
|
+
*/
|
|
1291
|
+
findLengthUnitReference(preferredUnitName, effective) {
|
|
1183
1292
|
if (!this.entityExtractor)
|
|
1184
1293
|
return null;
|
|
1185
|
-
|
|
1186
|
-
|
|
1294
|
+
// Only source records carry the bytes `extractEntity` reads, so an
|
|
1295
|
+
// overlay-created project is skipped rather than shadowing the file's own.
|
|
1296
|
+
const projectId = (effective.byType.get('IFCPROJECT') ?? []).find((id) => this.dataStore.entityIndex.byId.has(id));
|
|
1297
|
+
const projectRef = projectId !== undefined ? this.dataStore.entityIndex.byId.get(projectId) : undefined;
|
|
1187
1298
|
const project = projectRef ? this.entityExtractor.extractEntity(projectRef) : null;
|
|
1188
1299
|
const unitAssignmentId = project?.attributes?.[8];
|
|
1189
|
-
if (typeof unitAssignmentId !== 'number')
|
|
1300
|
+
if (typeof unitAssignmentId !== 'number' || effective.isDeleted(unitAssignmentId))
|
|
1190
1301
|
return null;
|
|
1191
1302
|
const unitAssignmentRef = this.dataStore.entityIndex.byId.get(unitAssignmentId);
|
|
1192
1303
|
const unitAssignment = unitAssignmentRef ? this.entityExtractor.extractEntity(unitAssignmentRef) : null;
|
|
@@ -1194,7 +1305,7 @@ export class StepExporter {
|
|
|
1194
1305
|
if (!Array.isArray(units))
|
|
1195
1306
|
return null;
|
|
1196
1307
|
for (const unitId of units) {
|
|
1197
|
-
if (typeof unitId !== 'number')
|
|
1308
|
+
if (typeof unitId !== 'number' || effective.isDeleted(unitId))
|
|
1198
1309
|
continue;
|
|
1199
1310
|
const unitRef = this.dataStore.entityIndex.byId.get(unitId);
|
|
1200
1311
|
const unit = unitRef ? this.entityExtractor.extractEntity(unitRef) : null;
|
|
@@ -1222,10 +1333,36 @@ export class StepExporter {
|
|
|
1222
1333
|
}
|
|
1223
1334
|
return null;
|
|
1224
1335
|
}
|
|
1225
|
-
|
|
1336
|
+
/**
|
|
1337
|
+
* Record that a requested IfcMapConversion could not be written. Emitting it
|
|
1338
|
+
* anyway would leave `SourceCRS` pointing at nothing, so the refusal is the
|
|
1339
|
+
* correct output — but the file alone cannot express it, which is why it goes
|
|
1340
|
+
* back to the caller in `stats.warnings` as well as to the console (#2067).
|
|
1341
|
+
*/
|
|
1342
|
+
reportMapConversionRefused(warnings) {
|
|
1343
|
+
warnings.push(MAP_CONVERSION_WITHOUT_CONTEXT_WARNING);
|
|
1344
|
+
console.warn(`[StepExporter] ${MAP_CONVERSION_WITHOUT_CONTEXT_WARNING}`);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Record that a requested IfcMapConversion could not be written because
|
|
1348
|
+
* there is no IfcProjectedCRS to attach it to — a different refusal from
|
|
1349
|
+
* {@link reportMapConversionRefused}: "no CRS to attach it to" rather than
|
|
1350
|
+
* "no context to reference" (#2105).
|
|
1351
|
+
*/
|
|
1352
|
+
reportMapConversionRefusedNoCrs(warnings) {
|
|
1353
|
+
warnings.push(MAP_CONVERSION_WITHOUT_CRS_WARNING);
|
|
1354
|
+
console.warn(`[StepExporter] ${MAP_CONVERSION_WITHOUT_CRS_WARNING}`);
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* `effective` again: the id returned here becomes the new IfcMapConversion's
|
|
1358
|
+
* SourceCRS, so a tombstoned context would leave the created line pointing at
|
|
1359
|
+
* a record the export skips — a dangling reference and an invalid file.
|
|
1360
|
+
*/
|
|
1361
|
+
findPreferredGeometricRepresentationContextId(effective) {
|
|
1226
1362
|
if (!this.entityExtractor)
|
|
1227
1363
|
return null;
|
|
1228
|
-
const contextIds =
|
|
1364
|
+
const contextIds = (effective.byType.get('IFCGEOMETRICREPRESENTATIONCONTEXT') ?? [])
|
|
1365
|
+
.filter((id) => this.dataStore.entityIndex.byId.has(id));
|
|
1229
1366
|
let first3dContext = null;
|
|
1230
1367
|
for (const contextId of contextIds) {
|
|
1231
1368
|
const contextRef = this.dataStore.entityIndex.byId.get(contextId);
|
|
@@ -1263,8 +1400,8 @@ export class StepExporter {
|
|
|
1263
1400
|
/**
|
|
1264
1401
|
* Find a unit entity ID by name (simplified - returns null for now)
|
|
1265
1402
|
*/
|
|
1266
|
-
findUnitId(unitName) {
|
|
1267
|
-
return this.findLengthUnitReference(this.normalizeMapUnitName(unitName));
|
|
1403
|
+
findUnitId(unitName, effective) {
|
|
1404
|
+
return this.findLengthUnitReference(this.normalizeMapUnitName(unitName), effective);
|
|
1268
1405
|
}
|
|
1269
1406
|
/**
|
|
1270
1407
|
* Check if an entity type is a geometry-related type
|
|
@@ -1343,7 +1480,7 @@ export class StepExporter {
|
|
|
1343
1480
|
const entityRef = this.dataStore.entityIndex.byId.get(relId);
|
|
1344
1481
|
if (!entityRef || !this.dataStore.source)
|
|
1345
1482
|
return [];
|
|
1346
|
-
const entityText =
|
|
1483
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1347
1484
|
// Parse IfcRelDefinesByProperties: #ID=IFCRELDEFINESBYPROPERTIES('guid',$,$,$,(#objects),#pset);
|
|
1348
1485
|
// The 5th argument (index 4) is the list of related objects
|
|
1349
1486
|
const match = entityText.match(/\(([^)]+)\)\s*,\s*#(\d+)\s*\)\s*;/);
|
|
@@ -1364,7 +1501,7 @@ export class StepExporter {
|
|
|
1364
1501
|
const entityRef = this.dataStore.entityIndex.byId.get(relId);
|
|
1365
1502
|
if (!entityRef || !this.dataStore.source)
|
|
1366
1503
|
return null;
|
|
1367
|
-
const entityText =
|
|
1504
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1368
1505
|
// Last #ID before the closing );
|
|
1369
1506
|
const match = entityText.match(/,\s*#(\d+)\s*\)\s*;$/);
|
|
1370
1507
|
if (!match)
|
|
@@ -1378,7 +1515,7 @@ export class StepExporter {
|
|
|
1378
1515
|
const entityRef = this.dataStore.entityIndex.byId.get(psetId);
|
|
1379
1516
|
if (!entityRef || !this.dataStore.source)
|
|
1380
1517
|
return null;
|
|
1381
|
-
const entityText =
|
|
1518
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1382
1519
|
// Parse: IFCPROPERTYSET('guid',$,'Name',$,...) - Name is 3rd argument
|
|
1383
1520
|
const match = entityText.match(/IFCPROPERTYSET\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
1384
1521
|
if (!match)
|
|
@@ -1392,7 +1529,7 @@ export class StepExporter {
|
|
|
1392
1529
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1393
1530
|
if (!entityRef || !this.dataStore.source)
|
|
1394
1531
|
return null;
|
|
1395
|
-
const entityText =
|
|
1532
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1396
1533
|
// Parse: IFCELEMENTQUANTITY('guid',$,'Name',...) - Name is 3rd argument
|
|
1397
1534
|
const match = entityText.match(/IFCELEMENTQUANTITY\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
1398
1535
|
if (!match)
|
|
@@ -1440,7 +1577,7 @@ export class StepExporter {
|
|
|
1440
1577
|
const entityRef = this.dataStore.entityIndex.byId.get(psetId);
|
|
1441
1578
|
if (!entityRef || !this.dataStore.source)
|
|
1442
1579
|
return [];
|
|
1443
|
-
const entityText =
|
|
1580
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1444
1581
|
// Parse: IFCPROPERTYSET(...,(#prop1,#prop2,...)); - Last argument is properties list
|
|
1445
1582
|
const match = entityText.match(/\(\s*(#[^)]+)\s*\)\s*\)\s*;$/);
|
|
1446
1583
|
if (!match)
|
|
@@ -1487,7 +1624,7 @@ export class StepExporter {
|
|
|
1487
1624
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1488
1625
|
if (!entityRef || !this.dataStore.source)
|
|
1489
1626
|
return null;
|
|
1490
|
-
const entityText =
|
|
1627
|
+
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1491
1628
|
const match = entityText.match(/^(#\d+\s*=\s*\w+\()([\s\S]*)(\)\s*;)\s*$/);
|
|
1492
1629
|
if (!match)
|
|
1493
1630
|
return null;
|