@ifc-lite/export 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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/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 +387 -102
- 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,24 +125,43 @@ 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();
|
|
138
|
+
// Attribute values come from the *overlay*, never from the mutation
|
|
139
|
+
// history. The history is append-only and undo writes its reverse edit
|
|
140
|
+
// with `skipHistory: true`, so a superseded UPDATE_ATTRIBUTE record keeps
|
|
141
|
+
// its stale `newValue` forever — replaying it resurrects edits the user
|
|
142
|
+
// undid (#1957). The overlay is what the editor shows, and it is already
|
|
143
|
+
// the source for psets, quantities, positional attributes and retypes
|
|
144
|
+
// below, so attributes were the sole outlier.
|
|
145
|
+
for (const [entityId, attrs] of this.mutationView.getAttributeMutationsByEntity()) {
|
|
146
|
+
modifiedEntities.add(entityId);
|
|
147
|
+
let target = modifiedAttributes.get(entityId);
|
|
148
|
+
if (!target) {
|
|
149
|
+
target = new Map();
|
|
150
|
+
modifiedAttributes.set(entityId, target);
|
|
151
|
+
}
|
|
152
|
+
for (const [name, value] of attrs)
|
|
153
|
+
target.set(name, value);
|
|
154
|
+
}
|
|
114
155
|
// Group mutations by entity, separating property vs quantity mutations
|
|
115
156
|
const entityPropMutations = new Map();
|
|
116
157
|
const entityQuantMutations = new Map();
|
|
117
158
|
for (const mutation of mutations) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
modifiedAttributes.get(mutation.entityId).set(mutation.attributeName, mutation.newValue == null ? '' : String(mutation.newValue));
|
|
159
|
+
// Handled above, off the overlay. Skipped explicitly because an
|
|
160
|
+
// UPDATE_ATTRIBUTE record can also carry a `psetName` (georef fields
|
|
161
|
+
// encode their target entity there) and must not be mistaken for a
|
|
162
|
+
// property-set edit.
|
|
163
|
+
if (mutation.type === 'UPDATE_ATTRIBUTE')
|
|
124
164
|
continue;
|
|
125
|
-
}
|
|
126
165
|
if (!mutation.psetName)
|
|
127
166
|
continue;
|
|
128
167
|
const isQuantity = mutation.type === 'CREATE_QUANTITY' || mutation.type === 'UPDATE_QUANTITY' || mutation.type === 'DELETE_QUANTITY';
|
|
@@ -137,12 +176,48 @@ export class StepExporter {
|
|
|
137
176
|
// below previously walked every entity in `entityIndex.byId` per
|
|
138
177
|
// modified entity (O(E·N)); the index keeps the per-entity step
|
|
139
178
|
// O(K) where K is the number of rels referencing that entity.
|
|
140
|
-
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
|
+
}
|
|
141
190
|
// Collect modified property sets and find original psets to skip
|
|
142
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;
|
|
143
212
|
modifiedEntities.add(entityId);
|
|
144
213
|
modifiedPsets.set(entityId, psetNames);
|
|
145
|
-
|
|
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++;
|
|
146
221
|
// Get the FULL mutated property sets for this entity (merged base + mutations)
|
|
147
222
|
const allPsets = this.mutationView.getForEntity(entityId);
|
|
148
223
|
const relevantPsets = allPsets.filter((pset) => psetNames.has(pset.name));
|
|
@@ -171,8 +246,8 @@ export class StepExporter {
|
|
|
171
246
|
}
|
|
172
247
|
}
|
|
173
248
|
}
|
|
174
|
-
if (
|
|
175
|
-
const typeOwnedPsetIds = this.getTypeOwnedHasPropertySetIds(entityId);
|
|
249
|
+
if (isTypeClass(effective.typeOf(entityId))) {
|
|
250
|
+
const typeOwnedPsetIds = this.getTypeOwnedHasPropertySetIds(entityId, effective);
|
|
176
251
|
const typeOwnedAffected = new Set();
|
|
177
252
|
for (const psetId of typeOwnedPsetIds) {
|
|
178
253
|
const psetName = this.getPropertySetName(psetId);
|
|
@@ -201,8 +276,13 @@ export class StepExporter {
|
|
|
201
276
|
if (options.includeQuantities === false)
|
|
202
277
|
entityQuantMutations.clear();
|
|
203
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;
|
|
204
282
|
modifiedEntities.add(entityId);
|
|
205
|
-
|
|
283
|
+
// See the property loop above — an overlay-created entity is counted as
|
|
284
|
+
// new, not modified.
|
|
285
|
+
if (!isOverlayCreated(entityId) && !modifiedPsets.has(entityId))
|
|
206
286
|
modifiedEntityCount++;
|
|
207
287
|
const allQsets = this.mutationView.getQuantitiesForEntity(entityId);
|
|
208
288
|
const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
|
|
@@ -227,6 +307,12 @@ export class StepExporter {
|
|
|
227
307
|
}
|
|
228
308
|
}
|
|
229
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;
|
|
230
316
|
if (!entityPropMutations.has(entityId) && !entityQuantMutations.has(entityId)) {
|
|
231
317
|
modifiedEntityCount++;
|
|
232
318
|
}
|
|
@@ -397,20 +483,65 @@ export class StepExporter {
|
|
|
397
483
|
},
|
|
398
484
|
};
|
|
399
485
|
}
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
//
|
|
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).
|
|
405
493
|
let allowedEntityIds = null;
|
|
406
494
|
if (options.visibleOnly && this.dataStore.source) {
|
|
407
|
-
const { roots, hiddenProductIds } = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null);
|
|
408
|
-
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);
|
|
409
497
|
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
410
498
|
// geometry. Styled items reference geometry items but nothing references
|
|
411
499
|
// them back, so the forward closure misses them.
|
|
412
|
-
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId:
|
|
500
|
+
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId: effective, byType: effective.byType });
|
|
413
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
|
+
};
|
|
414
545
|
// A modified pset is replaced wholesale, which skips ALL of its member atoms.
|
|
415
546
|
// But IFC exporters deduplicate identical Pset_*Common atoms (e.g. one
|
|
416
547
|
// IsExternal IfcPropertySingleValue shared by dozens of psets), so skipping a
|
|
@@ -420,13 +551,10 @@ export class StepExporter {
|
|
|
420
551
|
// Export original entities from source buffer, SKIPPING modified property sets
|
|
421
552
|
if (!options.deltaOnly && this.dataStore.source) {
|
|
422
553
|
const source = this.dataStore.source;
|
|
423
|
-
// Extract existing entities from source
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (overlayActive && typeof this.mutationView.isDeleted === 'function' && this.mutationView.isDeleted(expressId)) {
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
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) {
|
|
430
558
|
// Skip overlay-only entities — emitted by the new-entities pass below
|
|
431
559
|
if (entityRef.byteLength === 0 || entityRef.byteOffset < 0) {
|
|
432
560
|
continue;
|
|
@@ -504,30 +632,43 @@ export class StepExporter {
|
|
|
504
632
|
}
|
|
505
633
|
}
|
|
506
634
|
// Generate new property entities for mutations (these REPLACE the skipped ones)
|
|
635
|
+
const generatedTypeOwnedPsetIds = new Map();
|
|
507
636
|
for (const { entityId, psets } of newPropertySets) {
|
|
508
|
-
|
|
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);
|
|
509
642
|
entities.push(...newEntities.lines);
|
|
510
643
|
newEntityCount += newEntities.count;
|
|
511
|
-
|
|
512
|
-
if (typeOwnedPsetNames && typeOwnedPsetNames.size > 0) {
|
|
513
|
-
const rewritten = this.rewriteTypeEntityHasPropertySets(entityId, typeOwnedPsetIdsByEntity.get(entityId) ?? [], typeOwnedPsetNames, newEntities.generatedTypeOwnedPsetIds);
|
|
514
|
-
if (rewritten) {
|
|
515
|
-
rewrittenEntityLines.set(entityId, rewritten);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
644
|
+
generatedTypeOwnedPsetIds.set(entityId, newEntities.generatedTypeOwnedPsetIds);
|
|
518
645
|
}
|
|
519
|
-
//
|
|
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.
|
|
520
650
|
for (const [entityId, typeOwnedPsetNames] of typeOwnedPsetNamesByEntity) {
|
|
521
|
-
|
|
651
|
+
// `entityId` here is a TYPE object rather than an element; `willBeEmitted`
|
|
652
|
+
// resolves either the same way (#2030).
|
|
653
|
+
if (!willBeEmitted(entityId))
|
|
522
654
|
continue;
|
|
523
|
-
const
|
|
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);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const rewritten = this.replaceEntityAttribute(entityId, HAS_PROPERTY_SETS_SLOT, hasPropertySetsToken(resolved));
|
|
524
663
|
if (rewritten) {
|
|
525
664
|
rewrittenEntityLines.set(entityId, rewritten);
|
|
526
665
|
}
|
|
527
666
|
}
|
|
528
667
|
// Generate new quantity entities for mutations
|
|
529
668
|
for (const { entityId, qsets } of newQuantitySets) {
|
|
530
|
-
|
|
669
|
+
if (!willBeEmitted(entityId))
|
|
670
|
+
continue;
|
|
671
|
+
const newEntities = this.generateQuantitySetEntities(entityId, qsets, willBeEmitted, options.guidRandom);
|
|
531
672
|
entities.push(...newEntities.lines);
|
|
532
673
|
newEntityCount += newEntities.count;
|
|
533
674
|
}
|
|
@@ -580,6 +721,32 @@ export class StepExporter {
|
|
|
580
721
|
else {
|
|
581
722
|
argsText = serializeEntityArgs(entity.type, entity.attributes, sourceSchema);
|
|
582
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
|
+
}
|
|
583
750
|
const line = `#${entity.expressId}=${upperType}(${argsText});`;
|
|
584
751
|
if (converting) {
|
|
585
752
|
const converted = convertStepLine(line, sourceSchema, schema, options.guidRandom);
|
|
@@ -647,28 +814,56 @@ export class StepExporter {
|
|
|
647
814
|
* MANDATORY in IFC2X3 (IfcRoot.OwnerHistory), so emitting `$` yields an invalid
|
|
648
815
|
* IFC2X3 file that strict readers (e.g. BIM Vision) reject.
|
|
649
816
|
*
|
|
650
|
-
* Prefer the host element's OWN owner history
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
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.
|
|
655
827
|
*/
|
|
656
|
-
resolveOwnerHistoryRef(hostEntityId,
|
|
828
|
+
resolveOwnerHistoryRef(hostEntityId, willBeEmitted) {
|
|
657
829
|
const own = this.getOwnerHistoryRefOfEntity(hostEntityId);
|
|
658
830
|
if (own !== null) {
|
|
659
831
|
const ownId = parseInt(own.slice(1), 10);
|
|
660
|
-
if (
|
|
832
|
+
if (willBeEmitted(ownId))
|
|
661
833
|
return own;
|
|
662
834
|
}
|
|
663
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.
|
|
664
839
|
const ids = this.dataStore.entityIndex.byType.get('IFCOWNERHISTORY') ?? [];
|
|
665
|
-
const surviving =
|
|
666
|
-
? ids[0]
|
|
667
|
-
: ids.find((id) => allowedEntityIds.has(id));
|
|
840
|
+
const surviving = ids.find((id) => willBeEmitted(id));
|
|
668
841
|
this.ownerHistoryFallbackRef = surviving !== undefined ? `#${surviving}` : '$';
|
|
669
842
|
}
|
|
670
843
|
return this.ownerHistoryFallbackRef;
|
|
671
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
|
+
}
|
|
672
867
|
/**
|
|
673
868
|
* Read an element's own OwnerHistory reference (`#id`), or null when the
|
|
674
869
|
* element omits one (`$`) or cannot be parsed. OwnerHistory is the second
|
|
@@ -679,6 +874,17 @@ export class StepExporter {
|
|
|
679
874
|
if (cached !== undefined)
|
|
680
875
|
return cached;
|
|
681
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
|
+
}
|
|
682
888
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
683
889
|
if (entityRef && this.dataStore.source && entityRef.byteLength > 0) {
|
|
684
890
|
const entityText = safeUtf8Decode(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
@@ -694,7 +900,7 @@ export class StepExporter {
|
|
|
694
900
|
/**
|
|
695
901
|
* Generate STEP entities for property sets
|
|
696
902
|
*/
|
|
697
|
-
generatePropertySetEntities(entityId, psets,
|
|
903
|
+
generatePropertySetEntities(entityId, psets, willBeEmitted, typeOwnedPsetNames, random) {
|
|
698
904
|
const lines = [];
|
|
699
905
|
let count = 0;
|
|
700
906
|
const generatedTypeOwnedPsetIds = new Map();
|
|
@@ -718,7 +924,7 @@ export class StepExporter {
|
|
|
718
924
|
const propRefs = propertyIds.map(id => `#${id}`).join(',');
|
|
719
925
|
const globalId = this.generateGlobalId(random);
|
|
720
926
|
// #ID=IFCPROPERTYSET('GlobalId',#ownerHistory,'Name',$,(#props));
|
|
721
|
-
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
927
|
+
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(pset.name)}',$,(${propRefs}));`;
|
|
722
928
|
lines.push(psetLine);
|
|
723
929
|
if (typeOwnedPsetNames?.has(pset.name)) {
|
|
724
930
|
generatedTypeOwnedPsetIds.set(pset.name, psetId);
|
|
@@ -729,7 +935,7 @@ export class StepExporter {
|
|
|
729
935
|
count++;
|
|
730
936
|
const relGlobalId = this.generateGlobalId(random);
|
|
731
937
|
// #ID=IFCRELDEFINESBYPROPERTIES('GlobalId',#ownerHistory,$,$,(#entity),#pset);
|
|
732
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
938
|
+
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${psetId});`;
|
|
733
939
|
lines.push(relLine);
|
|
734
940
|
}
|
|
735
941
|
}
|
|
@@ -738,7 +944,7 @@ export class StepExporter {
|
|
|
738
944
|
/**
|
|
739
945
|
* Generate STEP entities for quantity sets (IfcElementQuantity)
|
|
740
946
|
*/
|
|
741
|
-
generateQuantitySetEntities(entityId, qsets,
|
|
947
|
+
generateQuantitySetEntities(entityId, qsets, willBeEmitted, random) {
|
|
742
948
|
const lines = [];
|
|
743
949
|
let count = 0;
|
|
744
950
|
for (const qset of qsets) {
|
|
@@ -759,13 +965,13 @@ export class StepExporter {
|
|
|
759
965
|
const quantRefs = quantityIds.map(id => `#${id}`).join(',');
|
|
760
966
|
const globalId = this.generateGlobalId(random);
|
|
761
967
|
// #ID=IFCELEMENTQUANTITY('GlobalId',#ownerHistory,'Name',$,$,(#quants));
|
|
762
|
-
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
968
|
+
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
|
|
763
969
|
lines.push(qsetLine);
|
|
764
970
|
// Create IfcRelDefinesByProperties to link qset to entity
|
|
765
971
|
const relId = this.nextExpressId++;
|
|
766
972
|
count++;
|
|
767
973
|
const relGlobalId = this.generateGlobalId(random);
|
|
768
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId,
|
|
974
|
+
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${qsetId});`;
|
|
769
975
|
lines.push(relLine);
|
|
770
976
|
}
|
|
771
977
|
return { lines, count };
|
|
@@ -779,17 +985,28 @@ export class StepExporter {
|
|
|
779
985
|
if (openParen < 0 || closeParen < openParen) {
|
|
780
986
|
return entityText;
|
|
781
987
|
}
|
|
782
|
-
|
|
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);
|
|
783
994
|
if (attrNames.length === 0) {
|
|
784
995
|
return entityText;
|
|
785
996
|
}
|
|
786
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.
|
|
787
1001
|
let changed = false;
|
|
788
1002
|
for (const [attrName, value] of attributeMutations) {
|
|
789
1003
|
const index = attrNames.indexOf(attrName);
|
|
790
1004
|
if (index < 0 || index >= args.length)
|
|
791
1005
|
continue;
|
|
792
|
-
|
|
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]);
|
|
793
1010
|
changed = true;
|
|
794
1011
|
}
|
|
795
1012
|
if (!changed) {
|
|
@@ -797,6 +1014,92 @@ export class StepExporter {
|
|
|
797
1014
|
}
|
|
798
1015
|
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
799
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
|
+
}
|
|
800
1103
|
/**
|
|
801
1104
|
* Apply positional STEP argument overrides to an entity line.
|
|
802
1105
|
* Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
|
|
@@ -1007,25 +1310,31 @@ export class StepExporter {
|
|
|
1007
1310
|
* the source: for each related entity, list the rels and property/quantity
|
|
1008
1311
|
* sets that reference it. Used by the export pre-pass so the per-entity
|
|
1009
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.
|
|
1010
1316
|
*/
|
|
1011
1317
|
buildRelDefinesByPropertiesIndex() {
|
|
1012
|
-
const
|
|
1318
|
+
const byEntity = new Map();
|
|
1319
|
+
const relatedByRel = new Map();
|
|
1013
1320
|
for (const [relId, relRef] of this.dataStore.entityIndex.byId) {
|
|
1014
1321
|
if (relRef.type.toUpperCase() !== 'IFCRELDEFINESBYPROPERTIES')
|
|
1015
1322
|
continue;
|
|
1016
1323
|
const psetId = this.getRelatedPropertySet(relId);
|
|
1017
1324
|
if (!psetId)
|
|
1018
1325
|
continue;
|
|
1019
|
-
|
|
1020
|
-
|
|
1326
|
+
const related = this.getRelatedEntities(relId);
|
|
1327
|
+
relatedByRel.set(relId, related);
|
|
1328
|
+
for (const entityId of related) {
|
|
1329
|
+
let bucket = byEntity.get(entityId);
|
|
1021
1330
|
if (!bucket) {
|
|
1022
1331
|
bucket = [];
|
|
1023
|
-
|
|
1332
|
+
byEntity.set(entityId, bucket);
|
|
1024
1333
|
}
|
|
1025
1334
|
bucket.push({ relId, psetId });
|
|
1026
1335
|
}
|
|
1027
1336
|
}
|
|
1028
|
-
return
|
|
1337
|
+
return { byEntity, relatedByRel };
|
|
1029
1338
|
}
|
|
1030
1339
|
/**
|
|
1031
1340
|
* Get entity IDs related by IfcRelDefinesByProperties (the related objects)
|
|
@@ -1145,56 +1454,32 @@ export class StepExporter {
|
|
|
1145
1454
|
return ids;
|
|
1146
1455
|
}
|
|
1147
1456
|
/**
|
|
1148
|
-
*
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
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).
|
|
1157
1466
|
*/
|
|
1158
|
-
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
|
+
}
|
|
1159
1472
|
if (!this.entityExtractor)
|
|
1160
1473
|
return [];
|
|
1161
1474
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1162
1475
|
if (!entityRef)
|
|
1163
1476
|
return [];
|
|
1164
1477
|
const entity = this.entityExtractor.extractEntity(entityRef);
|
|
1165
|
-
const hasPropertySets = entity?.attributes?.[
|
|
1478
|
+
const hasPropertySets = entity?.attributes?.[HAS_PROPERTY_SETS_SLOT];
|
|
1166
1479
|
if (!Array.isArray(hasPropertySets))
|
|
1167
1480
|
return [];
|
|
1168
1481
|
return hasPropertySets.filter((value) => typeof value === 'number');
|
|
1169
1482
|
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Rewrite a type entity so its HasPropertySets attribute points to replacement psets.
|
|
1172
|
-
*/
|
|
1173
|
-
rewriteTypeEntityHasPropertySets(entityId, originalPsetIds, affectedPsetNames, replacementPsetIds) {
|
|
1174
|
-
const rewrittenIds = [];
|
|
1175
|
-
const usedReplacementNames = new Set();
|
|
1176
|
-
for (const psetId of originalPsetIds) {
|
|
1177
|
-
const psetName = this.getPropertySetName(psetId);
|
|
1178
|
-
if (psetName && affectedPsetNames.has(psetName)) {
|
|
1179
|
-
const replacementId = replacementPsetIds.get(psetName);
|
|
1180
|
-
if (replacementId !== undefined) {
|
|
1181
|
-
rewrittenIds.push(replacementId);
|
|
1182
|
-
usedReplacementNames.add(psetName);
|
|
1183
|
-
}
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
rewrittenIds.push(psetId);
|
|
1187
|
-
}
|
|
1188
|
-
for (const [psetName, psetId] of replacementPsetIds) {
|
|
1189
|
-
if (!usedReplacementNames.has(psetName)) {
|
|
1190
|
-
rewrittenIds.push(psetId);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
const attrValue = rewrittenIds.length > 0
|
|
1194
|
-
? `(${rewrittenIds.map(id => `#${id}`).join(',')})`
|
|
1195
|
-
: '$';
|
|
1196
|
-
return this.replaceEntityAttribute(entityId, 5, attrValue);
|
|
1197
|
-
}
|
|
1198
1483
|
/**
|
|
1199
1484
|
* Replace a single top-level STEP attribute in an entity line.
|
|
1200
1485
|
*/
|