@ifc-lite/export 2.7.1 → 2.8.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/dist/attribute-slot-types.d.ts +57 -0
- package/dist/attribute-slot-types.d.ts.map +1 -0
- package/dist/attribute-slot-types.js +236 -0
- package/dist/attribute-slot-types.js.map +1 -0
- package/dist/demesh-writer.d.ts.map +1 -1
- package/dist/demesh-writer.js +8 -4
- package/dist/demesh-writer.js.map +1 -1
- package/dist/effective-index.d.ts +93 -0
- package/dist/effective-index.d.ts.map +1 -0
- package/dist/effective-index.js +218 -0
- package/dist/effective-index.js.map +1 -0
- package/dist/lod0-generator.d.ts.map +1 -1
- package/dist/lod0-generator.js +10 -4
- package/dist/lod0-generator.js.map +1 -1
- package/dist/lod1-generator.d.ts.map +1 -1
- package/dist/lod1-generator.js +24 -10
- package/dist/lod1-generator.js.map +1 -1
- package/dist/reference-collector.d.ts +18 -2
- package/dist/reference-collector.d.ts.map +1 -1
- package/dist/reference-collector.js +57 -22
- package/dist/reference-collector.js.map +1 -1
- package/dist/step-exporter.d.ts +71 -15
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +365 -95
- package/dist/step-exporter.js.map +1 -1
- package/dist/type-owned-psets.d.ts +44 -0
- package/dist/type-owned-psets.d.ts.map +1 -0
- package/dist/type-owned-psets.js +94 -0
- package/dist/type-owned-psets.js.map +1 -0
- package/package.json +5 -5
package/dist/step-exporter.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
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,
|
|
4
|
+
import { EntityExtractor, generateHeader, parseSourceHeader, getAttributeNamesAcrossSchemas, serializeValue, ref, } from '@ifc-lite/parser';
|
|
5
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
9
|
import { retypeStepLine, retypeArgTokens } from './retype.js';
|
|
10
10
|
import { getCompleteEntityIndex, getMaxExpressId } from './entity-iteration.js';
|
|
11
|
+
import { authoredEntityRefs, getEffectiveEntityIndex } from './effective-index.js';
|
|
12
|
+
import { HAS_PROPERTY_SETS_SLOT, hasPropertySetsToken, isTypeClass, resolveTypeOwnedPsetIds, } from './type-owned-psets.js';
|
|
11
13
|
import { escapeStepString, toStepReal, quantityTypeToIfcType, serializePropertyValue, serializeAttributeValue, serializeStepValue, tokenIsRealLiteral, splitTopLevelArgs, splitTopLevelStepArguments, assembleStepBytes, } from './step-serialization.js';
|
|
12
14
|
import { getRealTypedSlots, serializeEntityArgs, serializeAttributeSlot, isTypedMarker } from './attribute-real-slots.js';
|
|
15
|
+
import { getEnumTypedSlots, getStringTypedSlots, serializeEnumToken, serializeStringSlot, } from './attribute-slot-types.js';
|
|
13
16
|
import { serializeQualifiedSelectSlot } from './select-qualification.js';
|
|
17
|
+
/** `OwnerHistory` is slot 1 on every `IfcRoot` subtype, all schemas. */
|
|
18
|
+
const OWNER_HISTORY_SLOT = 1;
|
|
14
19
|
/**
|
|
15
20
|
* IFC STEP file exporter
|
|
16
21
|
*/
|
|
@@ -41,6 +46,12 @@ export class StepExporter {
|
|
|
41
46
|
const entities = [];
|
|
42
47
|
let newEntityCount = 0;
|
|
43
48
|
let modifiedEntityCount = 0;
|
|
49
|
+
// Both owner-history caches are per-EXPORT, not per-exporter: they now
|
|
50
|
+
// depend on `willBeEmitted`, which depends on this call's options. Reusing
|
|
51
|
+
// one exporter for a `visibleOnly` export and then a full one would
|
|
52
|
+
// otherwise answer the second from the first one's closure.
|
|
53
|
+
this.ownerHistoryFallbackRef = undefined;
|
|
54
|
+
this.ownerHistoryByEntity.clear();
|
|
44
55
|
// Determine target schema from options, source schema from data store
|
|
45
56
|
const schema = options.schema || this.dataStore.schemaVersion || 'IFC4';
|
|
46
57
|
const sourceSchema = this.dataStore.schemaVersion || 'IFC4';
|
|
@@ -95,6 +106,15 @@ export class StepExporter {
|
|
|
95
106
|
timeStamp: options.timeStamp,
|
|
96
107
|
});
|
|
97
108
|
};
|
|
109
|
+
// The one authority for exists / class / deleted, overlay first and source
|
|
110
|
+
// buffer second. Every pass below asks this instead of `this.dataStore`,
|
|
111
|
+
// which answers only for the file as parsed (#2012).
|
|
112
|
+
const effective = getEffectiveEntityIndex(this.dataStore, this.mutationView, options.applyMutations !== false);
|
|
113
|
+
// Does this id belong to an entity the OVERLAY created (`createEntity` /
|
|
114
|
+
// `store.addEntity`) rather than to a record in the source buffer? Such an
|
|
115
|
+
// entity has no source bytes, so the source-iteration pass below never sees
|
|
116
|
+
// it and the new-entities pass at the end owns its line entirely (#2006).
|
|
117
|
+
const isOverlayCreated = (entityId) => effective.isOverlayCreated(entityId);
|
|
98
118
|
// Collect entities that need to be modified or created
|
|
99
119
|
const modifiedEntities = new Set();
|
|
100
120
|
const modifiedPsets = new Map(); // entityId -> psetNames being modified
|
|
@@ -105,9 +125,13 @@ export class StepExporter {
|
|
|
105
125
|
const typeOwnedPsetIdsByEntity = new Map();
|
|
106
126
|
const rewrittenEntityIds = new Set();
|
|
107
127
|
const rewrittenEntityLines = new Map();
|
|
128
|
+
/** HasPropertySets slot value for an OVERLAY-CREATED type object, applied
|
|
129
|
+
* by the new-entities pass (there is no source line to rewrite). */
|
|
130
|
+
const overlayTypeOwnedPsets = new Map();
|
|
108
131
|
// Track property set IDs and relationship IDs to skip
|
|
109
132
|
const skipPropertySetIds = new Set();
|
|
110
133
|
const skipRelationshipIds = new Set();
|
|
134
|
+
const overlayActive = !!this.mutationView && (options.applyMutations !== false);
|
|
111
135
|
// Process mutations if we have a mutation view
|
|
112
136
|
if (this.mutationView && (options.applyMutations !== false)) {
|
|
113
137
|
const mutations = this.mutationView.getMutations();
|
|
@@ -152,12 +176,48 @@ export class StepExporter {
|
|
|
152
176
|
// below previously walked every entity in `entityIndex.byId` per
|
|
153
177
|
// modified entity (O(E·N)); the index keeps the per-entity step
|
|
154
178
|
// O(K) where K is the number of rels referencing that entity.
|
|
155
|
-
const relDefinesByEntity = this.buildRelDefinesByPropertiesIndex();
|
|
179
|
+
const { byEntity: relDefinesByEntity, relatedByRel } = this.buildRelDefinesByPropertiesIndex();
|
|
180
|
+
// A source IfcRelDefinesByProperties whose EVERY related object the
|
|
181
|
+
// session deleted has nothing left to relate, and emitting it leaves a
|
|
182
|
+
// `#id` pointing at a record the export skipped. Dropped only when all of
|
|
183
|
+
// them are gone: a rel that still names a live entity is that entity's
|
|
184
|
+
// only link to its psets, and nothing here rewrites a RelatedObjects list.
|
|
185
|
+
for (const [relId, related] of relatedByRel) {
|
|
186
|
+
if (related.length > 0 && related.every((id) => effective.isDeleted(id))) {
|
|
187
|
+
skipRelationshipIds.add(relId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
156
190
|
// Collect modified property sets and find original psets to skip
|
|
157
191
|
for (const [entityId, psetNames] of entityPropMutations) {
|
|
192
|
+
// A deleted entity must not cause the exporter to REMOVE anything.
|
|
193
|
+
//
|
|
194
|
+
// This is the other half of the dangling-reference class, and the half
|
|
195
|
+
// `willBeEmitted` cannot reach: that predicate guards what gets ADDED,
|
|
196
|
+
// and this loop's real work is deciding what gets SKIPPED. An edited
|
|
197
|
+
// pset is replaced wholesale, so its original id goes into
|
|
198
|
+
// `skipPropertySetIds` — but IFC exporters share one IfcPropertySet
|
|
199
|
+
// between entities, and once the host is deleted there is no
|
|
200
|
+
// replacement to take its place. The surviving entity's relation then
|
|
201
|
+
// points at a container nobody wrote. Verified against main at
|
|
202
|
+
// e6516991 (#2030's own merge): edit `Pset_WallCommon` on one of two
|
|
203
|
+
// walls sharing it, delete that wall, and the export drops #11 while
|
|
204
|
+
// #12 still names it. `retainSharedAtoms` rescues a shared ATOM one
|
|
205
|
+
// level down; nothing rescues the shared container.
|
|
206
|
+
//
|
|
207
|
+
// Leaving the pset alone makes it an orphan when nothing else
|
|
208
|
+
// references it, which is valid IFC. Its relation is dropped by the
|
|
209
|
+
// sweep above, which handles a plain delete too — no pset edit needed.
|
|
210
|
+
if (effective.isDeleted(entityId))
|
|
211
|
+
continue;
|
|
158
212
|
modifiedEntities.add(entityId);
|
|
159
213
|
modifiedPsets.set(entityId, psetNames);
|
|
160
|
-
|
|
214
|
+
// Same rule as the attribute loop below: an overlay-CREATED entity is
|
|
215
|
+
// emitted once, by the new-entities pass, and already counted in
|
|
216
|
+
// `newEntityCount` — as are the pset entities this loop goes on to
|
|
217
|
+
// generate. Only the COUNT is guarded; the entity still records its
|
|
218
|
+
// pset edits and still emits them.
|
|
219
|
+
if (!isOverlayCreated(entityId))
|
|
220
|
+
modifiedEntityCount++;
|
|
161
221
|
// Get the FULL mutated property sets for this entity (merged base + mutations)
|
|
162
222
|
const allPsets = this.mutationView.getForEntity(entityId);
|
|
163
223
|
const relevantPsets = allPsets.filter((pset) => psetNames.has(pset.name));
|
|
@@ -186,8 +246,8 @@ export class StepExporter {
|
|
|
186
246
|
}
|
|
187
247
|
}
|
|
188
248
|
}
|
|
189
|
-
if (
|
|
190
|
-
const typeOwnedPsetIds = this.getTypeOwnedHasPropertySetIds(entityId);
|
|
249
|
+
if (isTypeClass(effective.typeOf(entityId))) {
|
|
250
|
+
const typeOwnedPsetIds = this.getTypeOwnedHasPropertySetIds(entityId, effective);
|
|
191
251
|
const typeOwnedAffected = new Set();
|
|
192
252
|
for (const psetId of typeOwnedPsetIds) {
|
|
193
253
|
const psetName = this.getPropertySetName(psetId);
|
|
@@ -216,8 +276,13 @@ export class StepExporter {
|
|
|
216
276
|
if (options.includeQuantities === false)
|
|
217
277
|
entityQuantMutations.clear();
|
|
218
278
|
for (const [entityId, qsetNames] of entityQuantMutations) {
|
|
279
|
+
// Same rule as the property loop above: a deleted entity removes nothing.
|
|
280
|
+
if (effective.isDeleted(entityId))
|
|
281
|
+
continue;
|
|
219
282
|
modifiedEntities.add(entityId);
|
|
220
|
-
|
|
283
|
+
// See the property loop above — an overlay-created entity is counted as
|
|
284
|
+
// new, not modified.
|
|
285
|
+
if (!isOverlayCreated(entityId) && !modifiedPsets.has(entityId))
|
|
221
286
|
modifiedEntityCount++;
|
|
222
287
|
const allQsets = this.mutationView.getQuantitiesForEntity(entityId);
|
|
223
288
|
const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
|
|
@@ -242,6 +307,12 @@ export class StepExporter {
|
|
|
242
307
|
}
|
|
243
308
|
}
|
|
244
309
|
for (const [entityId] of modifiedAttributes) {
|
|
310
|
+
// An overlay-CREATED entity carrying attribute edits is emitted once,
|
|
311
|
+
// by the new-entities pass, and already counted in `newEntityCount`.
|
|
312
|
+
// Counting it here too made the header claim two affected entities for
|
|
313
|
+
// one created-then-renamed wall.
|
|
314
|
+
if (isOverlayCreated(entityId))
|
|
315
|
+
continue;
|
|
245
316
|
if (!entityPropMutations.has(entityId) && !entityQuantMutations.has(entityId)) {
|
|
246
317
|
modifiedEntityCount++;
|
|
247
318
|
}
|
|
@@ -412,20 +483,65 @@ export class StepExporter {
|
|
|
412
483
|
},
|
|
413
484
|
};
|
|
414
485
|
}
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
//
|
|
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).
|
|
420
493
|
let allowedEntityIds = null;
|
|
421
494
|
if (options.visibleOnly && this.dataStore.source) {
|
|
422
|
-
const { roots, hiddenProductIds } = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null);
|
|
423
|
-
allowedEntityIds = collectReferencedEntityIds(roots, 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);
|
|
424
497
|
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
425
498
|
// geometry. Styled items reference geometry items but nothing references
|
|
426
499
|
// them back, so the forward closure misses them.
|
|
427
|
-
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId:
|
|
500
|
+
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId: effective, byType: effective.byType });
|
|
428
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Will this id have a defining STEP line in the output at all?
|
|
504
|
+
*
|
|
505
|
+
* The predicate is #2030's, and it is the right one: the pset, quantity and
|
|
506
|
+
* type-owned passes below are built from unfiltered mutation history, and
|
|
507
|
+
* what each of them needs to know before emitting an
|
|
508
|
+
* `IFCRELDEFINESBYPROPERTIES` is not "was this deleted" or "is this hidden"
|
|
509
|
+
* but the general question those are two answers to. A relation naming an
|
|
510
|
+
* expressId that never gets written is a dangling reference and an invalid
|
|
511
|
+
* file, whichever route dropped the line.
|
|
512
|
+
*
|
|
513
|
+
* #2030 had to reach for four things to answer it — a tombstone probe, a
|
|
514
|
+
* visibility set, a byte-range test on `completeIndex`, and a `getNewEntity`
|
|
515
|
+
* fallback whose stated purpose was that `deleteEntity` FORGOT an
|
|
516
|
+
* overlay-created entity instead of tombstoning it, so `isDeleted` could not
|
|
517
|
+
* answer for one. That fallback was documented on main as a workaround for
|
|
518
|
+
* exactly the model-level defect this branch fixes: `deleteEntity` now
|
|
519
|
+
* tombstones as well as forgets, so the effective index answers existence
|
|
520
|
+
* for source and overlay ids alike and the workaround collapses into it.
|
|
521
|
+
*
|
|
522
|
+
* The overlay branch does NOT disappear with it, and the distinction matters:
|
|
523
|
+
* `isOverlayCreated` is still load-bearing here, because a live
|
|
524
|
+
* overlay-created entity has no source bytes and would fail the byte-range
|
|
525
|
+
* test that a source record passes. What the tombstone fix removed is the
|
|
526
|
+
* need for that branch to double as a deletion detector.
|
|
527
|
+
*
|
|
528
|
+
* Deliberately unchanged from #2030 for source records under `deltaOnly` /
|
|
529
|
+
* `exportPropertiesOnly`: the source-iteration pass is skipped wholesale in
|
|
530
|
+
* those modes, yet a source entity still answers true here. A delta is a
|
|
531
|
+
* patch against a file that already has the line, not a standalone model.
|
|
532
|
+
*/
|
|
533
|
+
const willBeEmitted = (entityId) => {
|
|
534
|
+
if (allowedEntityIds !== null && !allowedEntityIds.has(entityId))
|
|
535
|
+
return false;
|
|
536
|
+
// Undefined for a tombstoned id and for one neither the file nor the
|
|
537
|
+
// session ever had — a stale mutation must not conjure a relation either.
|
|
538
|
+
const ref = effective.get(entityId);
|
|
539
|
+
if (!ref)
|
|
540
|
+
return false;
|
|
541
|
+
// An overlay-created record carries the placeholder byte range and is
|
|
542
|
+
// written by the new-entities pass; a source record needs real bytes.
|
|
543
|
+
return effective.isOverlayCreated(entityId) || (ref.byteLength > 0 && ref.byteOffset >= 0);
|
|
544
|
+
};
|
|
429
545
|
// A modified pset is replaced wholesale, which skips ALL of its member atoms.
|
|
430
546
|
// But IFC exporters deduplicate identical Pset_*Common atoms (e.g. one
|
|
431
547
|
// IsExternal IfcPropertySingleValue shared by dozens of psets), so skipping a
|
|
@@ -435,13 +551,10 @@ export class StepExporter {
|
|
|
435
551
|
// Export original entities from source buffer, SKIPPING modified property sets
|
|
436
552
|
if (!options.deltaOnly && this.dataStore.source) {
|
|
437
553
|
const source = this.dataStore.source;
|
|
438
|
-
// Extract existing entities from source
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (overlayActive && typeof this.mutationView.isDeleted === 'function' && this.mutationView.isDeleted(expressId)) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
554
|
+
// Extract existing entities from source. The effective index has already
|
|
555
|
+
// dropped everything the overlay tombstoned, so there is no separate
|
|
556
|
+
// deleted check to forget here.
|
|
557
|
+
for (const [expressId, entityRef] of effective) {
|
|
445
558
|
// Skip overlay-only entities — emitted by the new-entities pass below
|
|
446
559
|
if (entityRef.byteLength === 0 || entityRef.byteOffset < 0) {
|
|
447
560
|
continue;
|
|
@@ -519,30 +632,43 @@ export class StepExporter {
|
|
|
519
632
|
}
|
|
520
633
|
}
|
|
521
634
|
// Generate new property entities for mutations (these REPLACE the skipped ones)
|
|
635
|
+
const generatedTypeOwnedPsetIds = new Map();
|
|
522
636
|
for (const { entityId, psets } of newPropertySets) {
|
|
523
|
-
|
|
637
|
+
// Nothing may be emitted FOR an entity that gets no defining line —
|
|
638
|
+
// see `willBeEmitted` (#1978, #2030, #2012).
|
|
639
|
+
if (!willBeEmitted(entityId))
|
|
640
|
+
continue;
|
|
641
|
+
const newEntities = this.generatePropertySetEntities(entityId, psets, willBeEmitted, typeOwnedPsetNamesByEntity.get(entityId), options.guidRandom);
|
|
524
642
|
entities.push(...newEntities.lines);
|
|
525
643
|
newEntityCount += newEntities.count;
|
|
526
|
-
|
|
527
|
-
if (typeOwnedPsetNames && typeOwnedPsetNames.size > 0) {
|
|
528
|
-
const rewritten = this.rewriteTypeEntityHasPropertySets(entityId, typeOwnedPsetIdsByEntity.get(entityId) ?? [], typeOwnedPsetNames, newEntities.generatedTypeOwnedPsetIds);
|
|
529
|
-
if (rewritten) {
|
|
530
|
-
rewrittenEntityLines.set(entityId, rewritten);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
644
|
+
generatedTypeOwnedPsetIds.set(entityId, newEntities.generatedTypeOwnedPsetIds);
|
|
533
645
|
}
|
|
534
|
-
//
|
|
646
|
+
// Point every affected type object's HasPropertySets at the psets this
|
|
647
|
+
// export generated. One loop, because a type whose affected psets produced
|
|
648
|
+
// no replacement content (a deletion) needs exactly the same resolution
|
|
649
|
+
// with an empty replacement map.
|
|
535
650
|
for (const [entityId, typeOwnedPsetNames] of typeOwnedPsetNamesByEntity) {
|
|
536
|
-
|
|
651
|
+
// `entityId` here is a TYPE object rather than an element; `willBeEmitted`
|
|
652
|
+
// resolves either the same way (#2030).
|
|
653
|
+
if (!willBeEmitted(entityId))
|
|
654
|
+
continue;
|
|
655
|
+
const resolved = resolveTypeOwnedPsetIds(typeOwnedPsetIdsByEntity.get(entityId) ?? [], typeOwnedPsetNames, generatedTypeOwnedPsetIds.get(entityId) ?? new Map(), (psetId) => this.getPropertySetName(psetId));
|
|
656
|
+
if (effective.isOverlayCreated(entityId)) {
|
|
657
|
+
// No source line to rewrite: the new-entities pass writes this record
|
|
658
|
+
// from its authored payload, so the list rides in as a slot override.
|
|
659
|
+
overlayTypeOwnedPsets.set(entityId, resolved.length > 0 ? resolved.map((id) => `#${id}`) : null);
|
|
537
660
|
continue;
|
|
538
|
-
|
|
661
|
+
}
|
|
662
|
+
const rewritten = this.replaceEntityAttribute(entityId, HAS_PROPERTY_SETS_SLOT, hasPropertySetsToken(resolved));
|
|
539
663
|
if (rewritten) {
|
|
540
664
|
rewrittenEntityLines.set(entityId, rewritten);
|
|
541
665
|
}
|
|
542
666
|
}
|
|
543
667
|
// Generate new quantity entities for mutations
|
|
544
668
|
for (const { entityId, qsets } of newQuantitySets) {
|
|
545
|
-
|
|
669
|
+
if (!willBeEmitted(entityId))
|
|
670
|
+
continue;
|
|
671
|
+
const newEntities = this.generateQuantitySetEntities(entityId, qsets, willBeEmitted, options.guidRandom);
|
|
546
672
|
entities.push(...newEntities.lines);
|
|
547
673
|
newEntityCount += newEntities.count;
|
|
548
674
|
}
|
|
@@ -595,6 +721,32 @@ export class StepExporter {
|
|
|
595
721
|
else {
|
|
596
722
|
argsText = serializeEntityArgs(entity.type, entity.attributes, sourceSchema);
|
|
597
723
|
}
|
|
724
|
+
// Edits made AFTER the create live in the overlay, never in the
|
|
725
|
+
// authored payload (#2006). The source-iteration pass applies them to
|
|
726
|
+
// source records via applyAttributeMutations / applyPositionalMutations;
|
|
727
|
+
// an overlay-created entity has no source record, so without this it was
|
|
728
|
+
// written from its creation payload alone and every later
|
|
729
|
+
// `setAttribute` / `setPositionalAttribute` was silently dropped on
|
|
730
|
+
// save — data loss with no error and no warning.
|
|
731
|
+
//
|
|
732
|
+
// Order mirrors the source pass: retype (above) -> named attributes ->
|
|
733
|
+
// positional overrides, all resolved against the EFFECTIVE class.
|
|
734
|
+
const attributeOverrides = modifiedAttributes.get(entity.expressId) ?? null;
|
|
735
|
+
const queuedPositional = typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
736
|
+
? this.mutationView.getPositionalMutationsForEntity(entity.expressId)
|
|
737
|
+
: null;
|
|
738
|
+
// A created TYPE object owns its psets through HasPropertySets, and the
|
|
739
|
+
// ids of the psets this export generated are only known now — so they
|
|
740
|
+
// arrive as one more slot override rather than through the overlay.
|
|
741
|
+
// `has`, not `??`, for the same reason `overlaySlotValue` gives: the
|
|
742
|
+
// stored value is deliberately null when the resolved list is empty.
|
|
743
|
+
const positionalOverrides = overlayTypeOwnedPsets.has(entity.expressId)
|
|
744
|
+
? new Map(queuedPositional).set(HAS_PROPERTY_SETS_SLOT, overlayTypeOwnedPsets.get(entity.expressId) ?? null)
|
|
745
|
+
: queuedPositional;
|
|
746
|
+
if ((attributeOverrides && attributeOverrides.size > 0)
|
|
747
|
+
|| (positionalOverrides && positionalOverrides.size > 0)) {
|
|
748
|
+
argsText = this.applyOverlayEntityOverrides(argsText, upperType, attributeOverrides, positionalOverrides, sourceSchema);
|
|
749
|
+
}
|
|
598
750
|
const line = `#${entity.expressId}=${upperType}(${argsText});`;
|
|
599
751
|
if (converting) {
|
|
600
752
|
const converted = convertStepLine(line, sourceSchema, schema, options.guidRandom);
|
|
@@ -662,28 +814,56 @@ export class StepExporter {
|
|
|
662
814
|
* MANDATORY in IFC2X3 (IfcRoot.OwnerHistory), so emitting `$` yields an invalid
|
|
663
815
|
* IFC2X3 file that strict readers (e.g. BIM Vision) reject.
|
|
664
816
|
*
|
|
665
|
-
* Prefer the host element's OWN owner history
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
817
|
+
* Prefer the host element's OWN owner history, then any owner history that
|
|
818
|
+
* survives this export, then `$` only when none does.
|
|
819
|
+
*
|
|
820
|
+
* "Survives" is `willBeEmitted`, the same predicate that decides whether the
|
|
821
|
+
* host itself may have psets generated for it. A reference is a reference: it
|
|
822
|
+
* is no more acceptable to point an emitted `IfcPropertySet` at an owner
|
|
823
|
+
* history the session deleted than at a host it deleted. This used to consult
|
|
824
|
+
* only the `visibleOnly` closure, so an overlay-created OwnerHistory that was
|
|
825
|
+
* later deleted still got referenced — a dangling `#N`, reached through the
|
|
826
|
+
* one attribute the generators fill in for themselves.
|
|
670
827
|
*/
|
|
671
|
-
resolveOwnerHistoryRef(hostEntityId,
|
|
828
|
+
resolveOwnerHistoryRef(hostEntityId, willBeEmitted) {
|
|
672
829
|
const own = this.getOwnerHistoryRefOfEntity(hostEntityId);
|
|
673
830
|
if (own !== null) {
|
|
674
831
|
const ownId = parseInt(own.slice(1), 10);
|
|
675
|
-
if (
|
|
832
|
+
if (willBeEmitted(ownId))
|
|
676
833
|
return own;
|
|
677
834
|
}
|
|
678
835
|
if (this.ownerHistoryFallbackRef === undefined) {
|
|
836
|
+
// Source-only: the fallback is a best-effort "some owner history the file
|
|
837
|
+
// still has", and the host's OWN history above is the path that resolves
|
|
838
|
+
// an overlay-created one.
|
|
679
839
|
const ids = this.dataStore.entityIndex.byType.get('IFCOWNERHISTORY') ?? [];
|
|
680
|
-
const surviving =
|
|
681
|
-
? ids[0]
|
|
682
|
-
: ids.find((id) => allowedEntityIds.has(id));
|
|
840
|
+
const surviving = ids.find((id) => willBeEmitted(id));
|
|
683
841
|
this.ownerHistoryFallbackRef = surviving !== undefined ? `#${surviving}` : '$';
|
|
684
842
|
}
|
|
685
843
|
return this.ownerHistoryFallbackRef;
|
|
686
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* The overlay's answer for one positional slot of an overlay-created entity,
|
|
847
|
+
* falling back to the creation payload only when the overlay has NOTHING to
|
|
848
|
+
* say about that slot.
|
|
849
|
+
*
|
|
850
|
+
* **Ask `Map.has`, never `??`.** `setPositionalAttribute(id, slot, null)` is
|
|
851
|
+
* an explicit "clear this slot", and its value is `null`, so `??` reads the
|
|
852
|
+
* overlay's answer as an absence and reinstates the authored one. That is the
|
|
853
|
+
* same overlay-versus-buffer confusion this whole change is about, one
|
|
854
|
+
* attribute wide: an explicit null IS the overlay's answer, and the overlay is
|
|
855
|
+
* the authority. Cleared OwnerHistory came back as the authored reference, and
|
|
856
|
+
* a cleared `HasPropertySets` resurrected the list the user had removed.
|
|
857
|
+
*/
|
|
858
|
+
overlaySlotValue(entityId, slot, authored) {
|
|
859
|
+
const overrides = this.mutationView?.getPositionalMutationsForEntity(entityId);
|
|
860
|
+
if (!overrides?.has(slot))
|
|
861
|
+
return authored;
|
|
862
|
+
const value = overrides.get(slot);
|
|
863
|
+
// `Map.get` widens to `| undefined`, which `has` has already ruled out. A
|
|
864
|
+
// slot explicitly set to nothing serializes as `$`, i.e. null.
|
|
865
|
+
return value === undefined ? null : value;
|
|
866
|
+
}
|
|
687
867
|
/**
|
|
688
868
|
* Read an element's own OwnerHistory reference (`#id`), or null when the
|
|
689
869
|
* element omits one (`$`) or cannot be parsed. OwnerHistory is the second
|
|
@@ -694,6 +874,17 @@ export class StepExporter {
|
|
|
694
874
|
if (cached !== undefined)
|
|
695
875
|
return cached;
|
|
696
876
|
let result = null;
|
|
877
|
+
// An overlay-created host has no source line to read, but it does have an
|
|
878
|
+
// authored OwnerHistory in slot 1 — reading only the buffer sent every
|
|
879
|
+
// generated pset on a created entity to the file's first owner history
|
|
880
|
+
// instead of the one the caller named (#2012).
|
|
881
|
+
const overlay = this.mutationView?.getNewEntity(entityId);
|
|
882
|
+
if (overlay) {
|
|
883
|
+
const refs = authoredEntityRefs(this.overlaySlotValue(entityId, OWNER_HISTORY_SLOT, overlay.attributes[OWNER_HISTORY_SLOT]));
|
|
884
|
+
result = refs.length > 0 ? `#${refs[0]}` : null;
|
|
885
|
+
this.ownerHistoryByEntity.set(entityId, result);
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
697
888
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
698
889
|
if (entityRef && this.dataStore.source && entityRef.byteLength > 0) {
|
|
699
890
|
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
@@ -709,7 +900,7 @@ export class StepExporter {
|
|
|
709
900
|
/**
|
|
710
901
|
* Generate STEP entities for property sets
|
|
711
902
|
*/
|
|
712
|
-
generatePropertySetEntities(entityId, psets,
|
|
903
|
+
generatePropertySetEntities(entityId, psets, willBeEmitted, typeOwnedPsetNames, random) {
|
|
713
904
|
const lines = [];
|
|
714
905
|
let count = 0;
|
|
715
906
|
const generatedTypeOwnedPsetIds = new Map();
|
|
@@ -733,7 +924,7 @@ export class StepExporter {
|
|
|
733
924
|
const propRefs = propertyIds.map(id => `#${id}`).join(',');
|
|
734
925
|
const globalId = this.generateGlobalId(random);
|
|
735
926
|
// #ID=IFCPROPERTYSET('GlobalId',#ownerHistory,'Name',$,(#props));
|
|
736
|
-
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
927
|
+
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(pset.name)}',$,(${propRefs}));`;
|
|
737
928
|
lines.push(psetLine);
|
|
738
929
|
if (typeOwnedPsetNames?.has(pset.name)) {
|
|
739
930
|
generatedTypeOwnedPsetIds.set(pset.name, psetId);
|
|
@@ -744,7 +935,7 @@ export class StepExporter {
|
|
|
744
935
|
count++;
|
|
745
936
|
const relGlobalId = this.generateGlobalId(random);
|
|
746
937
|
// #ID=IFCRELDEFINESBYPROPERTIES('GlobalId',#ownerHistory,$,$,(#entity),#pset);
|
|
747
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
938
|
+
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${psetId});`;
|
|
748
939
|
lines.push(relLine);
|
|
749
940
|
}
|
|
750
941
|
}
|
|
@@ -753,7 +944,7 @@ export class StepExporter {
|
|
|
753
944
|
/**
|
|
754
945
|
* Generate STEP entities for quantity sets (IfcElementQuantity)
|
|
755
946
|
*/
|
|
756
|
-
generateQuantitySetEntities(entityId, qsets,
|
|
947
|
+
generateQuantitySetEntities(entityId, qsets, willBeEmitted, random) {
|
|
757
948
|
const lines = [];
|
|
758
949
|
let count = 0;
|
|
759
950
|
for (const qset of qsets) {
|
|
@@ -774,13 +965,13 @@ export class StepExporter {
|
|
|
774
965
|
const quantRefs = quantityIds.map(id => `#${id}`).join(',');
|
|
775
966
|
const globalId = this.generateGlobalId(random);
|
|
776
967
|
// #ID=IFCELEMENTQUANTITY('GlobalId',#ownerHistory,'Name',$,$,(#quants));
|
|
777
|
-
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
968
|
+
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
|
|
778
969
|
lines.push(qsetLine);
|
|
779
970
|
// Create IfcRelDefinesByProperties to link qset to entity
|
|
780
971
|
const relId = this.nextExpressId++;
|
|
781
972
|
count++;
|
|
782
973
|
const relGlobalId = this.generateGlobalId(random);
|
|
783
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
974
|
+
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${qsetId});`;
|
|
784
975
|
lines.push(relLine);
|
|
785
976
|
}
|
|
786
977
|
return { lines, count };
|
|
@@ -794,17 +985,28 @@ export class StepExporter {
|
|
|
794
985
|
if (openParen < 0 || closeParen < openParen) {
|
|
795
986
|
return entityText;
|
|
796
987
|
}
|
|
797
|
-
|
|
988
|
+
// Cross-schema, not the IFC4 pin: an IFC4X3-only class (IfcCourse, IfcRoad,
|
|
989
|
+
// IfcBridge, …) resolves no slots under the pin, so every named edit on one
|
|
990
|
+
// was silently discarded here too. Identical for the 755 pinned classes
|
|
991
|
+
// that declare attributes — `attribute-slot-types.test.ts` measures that —
|
|
992
|
+
// so no IFC4 export changes; this only stops dropping edits it used to drop.
|
|
993
|
+
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
798
994
|
if (attrNames.length === 0) {
|
|
799
995
|
return entityText;
|
|
800
996
|
}
|
|
801
997
|
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
998
|
+
// A source line NEVER pads (unlike the overlay-created path): a short
|
|
999
|
+
// argument list here means the file speaks a different schema, and growing
|
|
1000
|
+
// a record we did not author would corrupt it.
|
|
802
1001
|
let changed = false;
|
|
803
1002
|
for (const [attrName, value] of attributeMutations) {
|
|
804
1003
|
const index = attrNames.indexOf(attrName);
|
|
805
1004
|
if (index < 0 || index >= args.length)
|
|
806
1005
|
continue;
|
|
807
|
-
|
|
1006
|
+
// The source path shares every `$`-slot hole with the overlay-created
|
|
1007
|
+
// path, because a source record has plenty of `$` slots of its own. Both
|
|
1008
|
+
// go through the one helper below.
|
|
1009
|
+
args[index] = this.serializeNamedAttribute(entityType, index, value, args[index]);
|
|
808
1010
|
changed = true;
|
|
809
1011
|
}
|
|
810
1012
|
if (!changed) {
|
|
@@ -812,6 +1014,92 @@ export class StepExporter {
|
|
|
812
1014
|
}
|
|
813
1015
|
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
814
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Serialize one NAMED attribute override into its slot — the single point
|
|
1019
|
+
* both the source-buffer rewrite and the overlay-created rewrite go through.
|
|
1020
|
+
*
|
|
1021
|
+
* `serializeAttributeValue` decides the STEP form by reading the token being
|
|
1022
|
+
* replaced, which is sound only while that token carries type information. A
|
|
1023
|
+
* `$` slot carries none, and both paths have plenty: a source record's
|
|
1024
|
+
* optional attributes are `$`, and overlay-created records pad missing slots
|
|
1025
|
+
* with `$`. So the declared type decides first, and inference is the fallback
|
|
1026
|
+
* for slots the schema does not classify (references, SELECTs, numerics),
|
|
1027
|
+
* where reading the old token is exactly the right heuristic.
|
|
1028
|
+
*/
|
|
1029
|
+
serializeNamedAttribute(entityType, index, value, currentToken) {
|
|
1030
|
+
if (getEnumTypedSlots(entityType).has(index))
|
|
1031
|
+
return serializeEnumToken(value);
|
|
1032
|
+
if (getStringTypedSlots(entityType).has(index))
|
|
1033
|
+
return serializeStringSlot(value);
|
|
1034
|
+
return serializeAttributeValue(value, currentToken);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Apply overlay attribute + positional overrides to an OVERLAY-CREATED
|
|
1038
|
+
* entity's argument list (#2006).
|
|
1039
|
+
*
|
|
1040
|
+
* Distinct from {@link applyAttributeMutations} / {@link applyPositionalMutations},
|
|
1041
|
+
* which rewrite a line read out of the source buffer. Here the whole line is
|
|
1042
|
+
* ours: it was serialized moments ago from the creation payload, so the
|
|
1043
|
+
* argument list is the authoring payload's, not the file's. That difference
|
|
1044
|
+
* is why this PADS — `entity_create` takes whatever positional list the
|
|
1045
|
+
* caller passes, so a wall authored with three arguments still has a real
|
|
1046
|
+
* `Tag` slot at index 7, and dropping the edit because the payload was short
|
|
1047
|
+
* would be the very data loss this fixes. The source-buffer path must not
|
|
1048
|
+
* pad: there a short line means a different schema, and growing a record we
|
|
1049
|
+
* did not author would corrupt it.
|
|
1050
|
+
*
|
|
1051
|
+
* Named and positional overrides resolve to a slot index up front and share
|
|
1052
|
+
* ONE padding rule. Two padding rules on one record is how the next bug
|
|
1053
|
+
* starts, and the argument for padding — the class is fixed at creation time,
|
|
1054
|
+
* so a short payload is partial authoring — never depended on which of the
|
|
1055
|
+
* two APIs queued the edit.
|
|
1056
|
+
*/
|
|
1057
|
+
applyOverlayEntityOverrides(argsText, entityType, attributeOverrides, positionalOverrides, schemaVersion) {
|
|
1058
|
+
const args = argsText.length > 0 ? splitTopLevelArgs(argsText) : [];
|
|
1059
|
+
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
1060
|
+
const named = [];
|
|
1061
|
+
for (const [attrName, value] of attributeOverrides ?? []) {
|
|
1062
|
+
const index = attrNames.indexOf(attrName);
|
|
1063
|
+
if (index >= 0)
|
|
1064
|
+
named.push([index, value]);
|
|
1065
|
+
}
|
|
1066
|
+
// Grow to the class's FULL declared arity as soon as any override names a
|
|
1067
|
+
// declared slot the creation payload never reached. Growing only as far as
|
|
1068
|
+
// the edited slot would emit eight arguments for an IfcWall that declares
|
|
1069
|
+
// nine: this parser tolerates the truncated record, a schema-validating
|
|
1070
|
+
// consumer rejects the file.
|
|
1071
|
+
//
|
|
1072
|
+
// An index PAST the declared layout is not a slot at all, so it cannot
|
|
1073
|
+
// justify growing the record and stays dropped — as does any override on a
|
|
1074
|
+
// class neither schema source knows, where there is no arity to grow to.
|
|
1075
|
+
let needsPad = named.some(([index]) => index >= args.length);
|
|
1076
|
+
if (!needsPad && positionalOverrides) {
|
|
1077
|
+
for (const [index] of positionalOverrides) {
|
|
1078
|
+
if (index >= args.length && index < attrNames.length) {
|
|
1079
|
+
needsPad = true;
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (needsPad) {
|
|
1085
|
+
while (args.length < attrNames.length)
|
|
1086
|
+
args.push('$');
|
|
1087
|
+
}
|
|
1088
|
+
// Every `named` index is < attrNames.length by construction, and padding
|
|
1089
|
+
// has taken args.length to at least that, so each one lands.
|
|
1090
|
+
for (const [index, value] of named) {
|
|
1091
|
+
args[index] = this.serializeNamedAttribute(entityType, index, value, args[index]);
|
|
1092
|
+
}
|
|
1093
|
+
if (positionalOverrides && positionalOverrides.size > 0) {
|
|
1094
|
+
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1095
|
+
for (const [index, value] of positionalOverrides) {
|
|
1096
|
+
if (index < 0 || index >= args.length)
|
|
1097
|
+
continue;
|
|
1098
|
+
args[index] = this.serializePositionalOverride(entityType, index, value, args[index], realSlots, schemaVersion);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return args.join(',');
|
|
1102
|
+
}
|
|
815
1103
|
/**
|
|
816
1104
|
* Apply positional STEP argument overrides to an entity line.
|
|
817
1105
|
* Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
|
|
@@ -1022,25 +1310,31 @@ export class StepExporter {
|
|
|
1022
1310
|
* the source: for each related entity, list the rels and property/quantity
|
|
1023
1311
|
* sets that reference it. Used by the export pre-pass so the per-entity
|
|
1024
1312
|
* "find owning rels" step is O(K) rather than O(N) per modified entity.
|
|
1313
|
+
*
|
|
1314
|
+
* `relatedByRel` is the same walk read the other way round, so the deleted-host
|
|
1315
|
+
* sweep costs nothing extra.
|
|
1025
1316
|
*/
|
|
1026
1317
|
buildRelDefinesByPropertiesIndex() {
|
|
1027
|
-
const
|
|
1318
|
+
const byEntity = new Map();
|
|
1319
|
+
const relatedByRel = new Map();
|
|
1028
1320
|
for (const [relId, relRef] of this.dataStore.entityIndex.byId) {
|
|
1029
1321
|
if (relRef.type.toUpperCase() !== 'IFCRELDEFINESBYPROPERTIES')
|
|
1030
1322
|
continue;
|
|
1031
1323
|
const psetId = this.getRelatedPropertySet(relId);
|
|
1032
1324
|
if (!psetId)
|
|
1033
1325
|
continue;
|
|
1034
|
-
|
|
1035
|
-
|
|
1326
|
+
const related = this.getRelatedEntities(relId);
|
|
1327
|
+
relatedByRel.set(relId, related);
|
|
1328
|
+
for (const entityId of related) {
|
|
1329
|
+
let bucket = byEntity.get(entityId);
|
|
1036
1330
|
if (!bucket) {
|
|
1037
1331
|
bucket = [];
|
|
1038
|
-
|
|
1332
|
+
byEntity.set(entityId, bucket);
|
|
1039
1333
|
}
|
|
1040
1334
|
bucket.push({ relId, psetId });
|
|
1041
1335
|
}
|
|
1042
1336
|
}
|
|
1043
|
-
return
|
|
1337
|
+
return { byEntity, relatedByRel };
|
|
1044
1338
|
}
|
|
1045
1339
|
/**
|
|
1046
1340
|
* Get entity IDs related by IfcRelDefinesByProperties (the related objects)
|
|
@@ -1160,56 +1454,32 @@ export class StepExporter {
|
|
|
1160
1454
|
return ids;
|
|
1161
1455
|
}
|
|
1162
1456
|
/**
|
|
1163
|
-
*
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1457
|
+
* The full HasPropertySets id list of a type object, from whichever authority
|
|
1458
|
+
* owns the record.
|
|
1459
|
+
*
|
|
1460
|
+
* Slot 5 is `HasPropertySets` on every `IfcTypeObject` subtype. For a source
|
|
1461
|
+
* record the list is parsed out of the file; for an overlay-created type it is
|
|
1462
|
+
* read off the authored payload, where a reference is the documented `'#42'`
|
|
1463
|
+
* string form. Reading only the source made every pset on a created
|
|
1464
|
+
* `IfcWallType` look unowned, which is how it ended up on an occurrence
|
|
1465
|
+
* relation instead (#2012).
|
|
1172
1466
|
*/
|
|
1173
|
-
getTypeOwnedHasPropertySetIds(entityId) {
|
|
1467
|
+
getTypeOwnedHasPropertySetIds(entityId, effective) {
|
|
1468
|
+
if (effective.isOverlayCreated(entityId)) {
|
|
1469
|
+
const authored = this.mutationView?.getNewEntity(entityId)?.attributes?.[HAS_PROPERTY_SETS_SLOT];
|
|
1470
|
+
return authoredEntityRefs(this.overlaySlotValue(entityId, HAS_PROPERTY_SETS_SLOT, authored));
|
|
1471
|
+
}
|
|
1174
1472
|
if (!this.entityExtractor)
|
|
1175
1473
|
return [];
|
|
1176
1474
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1177
1475
|
if (!entityRef)
|
|
1178
1476
|
return [];
|
|
1179
1477
|
const entity = this.entityExtractor.extractEntity(entityRef);
|
|
1180
|
-
const hasPropertySets = entity?.attributes?.[
|
|
1478
|
+
const hasPropertySets = entity?.attributes?.[HAS_PROPERTY_SETS_SLOT];
|
|
1181
1479
|
if (!Array.isArray(hasPropertySets))
|
|
1182
1480
|
return [];
|
|
1183
1481
|
return hasPropertySets.filter((value) => typeof value === 'number');
|
|
1184
1482
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Rewrite a type entity so its HasPropertySets attribute points to replacement psets.
|
|
1187
|
-
*/
|
|
1188
|
-
rewriteTypeEntityHasPropertySets(entityId, originalPsetIds, affectedPsetNames, replacementPsetIds) {
|
|
1189
|
-
const rewrittenIds = [];
|
|
1190
|
-
const usedReplacementNames = new Set();
|
|
1191
|
-
for (const psetId of originalPsetIds) {
|
|
1192
|
-
const psetName = this.getPropertySetName(psetId);
|
|
1193
|
-
if (psetName && affectedPsetNames.has(psetName)) {
|
|
1194
|
-
const replacementId = replacementPsetIds.get(psetName);
|
|
1195
|
-
if (replacementId !== undefined) {
|
|
1196
|
-
rewrittenIds.push(replacementId);
|
|
1197
|
-
usedReplacementNames.add(psetName);
|
|
1198
|
-
}
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
rewrittenIds.push(psetId);
|
|
1202
|
-
}
|
|
1203
|
-
for (const [psetName, psetId] of replacementPsetIds) {
|
|
1204
|
-
if (!usedReplacementNames.has(psetName)) {
|
|
1205
|
-
rewrittenIds.push(psetId);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
const attrValue = rewrittenIds.length > 0
|
|
1209
|
-
? `(${rewrittenIds.map(id => `#${id}`).join(',')})`
|
|
1210
|
-
: '$';
|
|
1211
|
-
return this.replaceEntityAttribute(entityId, 5, attrValue);
|
|
1212
|
-
}
|
|
1213
1483
|
/**
|
|
1214
1484
|
* Replace a single top-level STEP attribute in an entity line.
|
|
1215
1485
|
*/
|