@ifc-lite/export 2.8.3 → 2.8.5
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/declared-property-type.d.ts +107 -0
- package/dist/declared-property-type.d.ts.map +1 -0
- package/dist/declared-property-type.js +236 -0
- package/dist/declared-property-type.js.map +1 -0
- package/dist/delta-modification-ledger.d.ts +148 -0
- package/dist/delta-modification-ledger.d.ts.map +1 -0
- package/dist/delta-modification-ledger.js +194 -0
- package/dist/delta-modification-ledger.js.map +1 -0
- package/dist/demesh-session.d.ts.map +1 -1
- package/dist/demesh-session.js +7 -0
- package/dist/demesh-session.js.map +1 -1
- package/dist/in-place-nomination.d.ts +65 -0
- package/dist/in-place-nomination.d.ts.map +1 -0
- package/dist/in-place-nomination.js +27 -0
- package/dist/in-place-nomination.js.map +1 -0
- package/dist/merged-exporter.js +1 -1
- package/dist/retype.d.ts.map +1 -1
- package/dist/retype.js +2 -1
- package/dist/retype.js.map +1 -1
- package/dist/select-qualification.d.ts +12 -0
- package/dist/select-qualification.d.ts.map +1 -1
- package/dist/select-qualification.js +12 -1
- package/dist/select-qualification.js.map +1 -1
- package/dist/source-ref-bounds.d.ts +53 -0
- package/dist/source-ref-bounds.d.ts.map +1 -0
- package/dist/source-ref-bounds.js +25 -0
- package/dist/source-ref-bounds.js.map +1 -0
- package/dist/step-argument-parser.d.ts +90 -0
- package/dist/step-argument-parser.d.ts.map +1 -0
- package/dist/step-argument-parser.js +207 -0
- package/dist/step-argument-parser.js.map +1 -0
- package/dist/step-exporter.d.ts +45 -4
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +465 -84
- package/dist/step-exporter.js.map +1 -1
- package/dist/step-file-assembly.d.ts +35 -0
- package/dist/step-file-assembly.d.ts.map +1 -0
- package/dist/step-file-assembly.js +102 -0
- package/dist/step-file-assembly.js.map +1 -0
- package/dist/step-serialization.d.ts +48 -48
- package/dist/step-serialization.d.ts.map +1 -1
- package/dist/step-serialization.js +90 -182
- package/dist/step-serialization.js.map +1 -1
- package/dist/type-owned-psets.d.ts +36 -0
- package/dist/type-owned-psets.d.ts.map +1 -1
- package/dist/type-owned-psets.js +41 -0
- package/dist/type-owned-psets.js.map +1 -1
- package/dist/unit-normalize.d.ts.map +1 -1
- package/dist/unit-normalize.js +13 -1
- package/dist/unit-normalize.js.map +1 -1
- package/package.json +7 -6
package/dist/step-exporter.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
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 { asSourceBytes, EntityExtractor, generateHeader, parseSourceHeader, getAttributeNamesAcrossSchemas, serializeValue, ref, } from '@ifc-lite/parser';
|
|
4
|
+
import { asSourceBytes, EntityExtractor, extractQuantitiesOnDemand, generateHeader, parseSourceHeader, getAttributeNamesAcrossSchemas, serializeValue, ref, } from '@ifc-lite/parser';
|
|
5
5
|
import { generateIfcGuid } from '@ifc-lite/encoding';
|
|
6
6
|
import { collectReferencedEntityIds, getVisibleEntityIds, collectStyleEntities } from './reference-collector.js';
|
|
7
7
|
import { convertStepLine, needsConversion } from './schema-converter.js';
|
|
8
8
|
import { retypeStepLine, retypeArgTokens } from './retype.js';
|
|
9
9
|
import { getCompleteEntityIndex, getMaxExpressId } from './entity-iteration.js';
|
|
10
|
+
import { createModificationLedger, recordSourceLineDelivery, } from './delta-modification-ledger.js';
|
|
11
|
+
import { nominateDeliveredInPlaceEdits } from './in-place-nomination.js';
|
|
12
|
+
import { createSourceRefReader } from './source-ref-bounds.js';
|
|
10
13
|
import { authoredEntityRefs, getEffectiveEntityIndex } from './effective-index.js';
|
|
11
|
-
import { HAS_PROPERTY_SETS_SLOT,
|
|
12
|
-
import { escapeStepString, toStepReal, quantityTypeToIfcType,
|
|
14
|
+
import { HAS_PROPERTY_SETS_SLOT, isTypeClass, resolveTypeOwnedPsetIds, rewriteTypeOwnedPsetLine, typeOwnedPsetRewriteWarning, } from './type-owned-psets.js';
|
|
15
|
+
import { escapeStepString, toStepReal, quantityTypeToIfcType, serializeAttributeValue, serializeStepValue, tokenIsRealLiteral, } from './step-serialization.js';
|
|
16
|
+
import { splitTopLevelArgs } from './step-argument-parser.js';
|
|
17
|
+
import { assembleStepBytes } from './step-file-assembly.js';
|
|
13
18
|
import { getRealTypedSlots, serializeEntityArgs, serializeAttributeSlot, isTypedMarker } from './attribute-real-slots.js';
|
|
14
19
|
import { getEnumTypedSlots, getStringTypedSlots, serializeEnumToken, serializeStringSlot, } from './attribute-slot-types.js';
|
|
15
20
|
import { serializeQualifiedSelectSlot } from './select-qualification.js';
|
|
21
|
+
import { serializeNominalValue } from './declared-property-type.js';
|
|
16
22
|
/**
|
|
17
23
|
* UTF-8 decode of `[start, end)` of the source, accepting either the raw bytes
|
|
18
24
|
* or the {@link IfcSourceBytes} accessor (#2183). Replaces the direct
|
|
@@ -25,6 +31,21 @@ function decodeRange(src, start, end) {
|
|
|
25
31
|
}
|
|
26
32
|
/** `OwnerHistory` is slot 1 on every `IfcRoot` subtype, all schemas. */
|
|
27
33
|
const OWNER_HISTORY_SLOT = 1;
|
|
34
|
+
/**
|
|
35
|
+
* The store the extractor THIS class installed on a view currently reads
|
|
36
|
+
* (#2487). The extractor is installed once per view and closes over this box
|
|
37
|
+
* rather than over a store directly, so a later export of the same view against
|
|
38
|
+
* a different store re-points it instead of answering from the first file.
|
|
39
|
+
*
|
|
40
|
+
* A box, and not a `WeakSet` of views, because ownership has to reflect the
|
|
41
|
+
* CURRENT state and not the historical fact that an export once installed
|
|
42
|
+
* something. `setQuantityExtractor` is public: a caller may install its own
|
|
43
|
+
* afterwards, and a marker saying "the exporter owns this view" would then keep
|
|
44
|
+
* overwriting a caller-supplied base forever. With a box, the second export
|
|
45
|
+
* writes to a box nothing reads any more and never calls the setter again, so
|
|
46
|
+
* the caller's extractor stands. Weak, so it never keeps a session alive.
|
|
47
|
+
*/
|
|
48
|
+
const exporterQuantityBase = new WeakMap();
|
|
28
49
|
/**
|
|
29
50
|
* Message for the one refusal `export()` can report, shared by the returned
|
|
30
51
|
* `stats.warnings` entry and the console line so the two cannot drift.
|
|
@@ -68,7 +89,6 @@ export class StepExporter {
|
|
|
68
89
|
export(options) {
|
|
69
90
|
const entities = [];
|
|
70
91
|
let newEntityCount = 0;
|
|
71
|
-
let modifiedEntityCount = 0;
|
|
72
92
|
// Both owner-history caches are per-EXPORT, not per-exporter: they now
|
|
73
93
|
// depend on `willBeEmitted`, which depends on this call's options. Reusing
|
|
74
94
|
// one exporter for a `visibleOnly` export and then a full one would
|
|
@@ -140,6 +160,11 @@ export class StepExporter {
|
|
|
140
160
|
// entity has no source bytes, so the source-iteration pass below never sees
|
|
141
161
|
// it and the new-entities pass at the end owns its line entirely (#2006).
|
|
142
162
|
const isOverlayCreated = (entityId) => effective.isOverlayCreated(entityId);
|
|
163
|
+
// Does this record describe a line this export can actually READ out of the
|
|
164
|
+
// source? One predicate for every byte-range gate below, so they cannot
|
|
165
|
+
// disagree — see `source-ref-bounds.ts` for the corrupt file the weaker
|
|
166
|
+
// "is there a source / does the ref claim bytes" pair let through (#2491).
|
|
167
|
+
const isReadableSourceRef = createSourceRefReader(this.dataStore.source);
|
|
143
168
|
// Build visible-only closure if requested. Classification, the closure walk
|
|
144
169
|
// and the style pass all run over the EFFECTIVE index: an overlay-created
|
|
145
170
|
// product becomes a root by the same type rules as a parsed one, the walk
|
|
@@ -188,15 +213,35 @@ export class StepExporter {
|
|
|
188
213
|
if (allowedEntityIds !== null && !allowedEntityIds.has(entityId))
|
|
189
214
|
return false;
|
|
190
215
|
const ref = effective.get(entityId);
|
|
191
|
-
|
|
216
|
+
// The ref must be READABLE, not merely non-empty: a range this source
|
|
217
|
+
// cannot address decodes to the empty string, which used to be pushed
|
|
218
|
+
// into the file as a blank line while everything generated FOR the host
|
|
219
|
+
// still named it (#2491).
|
|
220
|
+
if (!ref || !isReadableSourceRef(ref))
|
|
192
221
|
return false;
|
|
193
222
|
if (options.deltaOnly !== true && isGeometryExcluded(entityId, ref.type))
|
|
194
223
|
return false;
|
|
195
224
|
return true;
|
|
196
225
|
};
|
|
226
|
+
// Under `deltaOnly` a nomination only becomes a count once some pass has
|
|
227
|
+
// actually written content that delivers THAT KIND of edit for the host —
|
|
228
|
+
// see `delta-modification-ledger.ts` for why the two are not the same event
|
|
229
|
+
// in that mode, and why the pair is (entity, kind) rather than the entity
|
|
230
|
+
// (#2462).
|
|
231
|
+
const modifications = createModificationLedger(options.deltaOnly === true);
|
|
232
|
+
/**
|
|
233
|
+
* Hosts whose in-place named-attribute edits a FULL export may count, per
|
|
234
|
+
* kind. Filled by the collection passes below and read by the two passes
|
|
235
|
+
* that write a rewritten source line — see `in-place-nomination.ts` for why
|
|
236
|
+
* the nomination waits for the rewrite in this mode and not under
|
|
237
|
+
* `deltaOnly` (#2483).
|
|
238
|
+
*/
|
|
239
|
+
const inPlaceNominees = {
|
|
240
|
+
attribute: new Set(),
|
|
241
|
+
georeferencing: new Set(),
|
|
242
|
+
};
|
|
197
243
|
// Collect entities that need to be modified or created
|
|
198
244
|
const modifiedEntities = new Set();
|
|
199
|
-
const modifiedPsets = new Map(); // entityId -> psetNames being modified
|
|
200
245
|
const modifiedAttributes = new Map();
|
|
201
246
|
const newPropertySets = [];
|
|
202
247
|
const newQuantitySets = [];
|
|
@@ -289,14 +334,22 @@ export class StepExporter {
|
|
|
289
334
|
if (effective.isDeleted(entityId))
|
|
290
335
|
continue;
|
|
291
336
|
modifiedEntities.add(entityId);
|
|
292
|
-
modifiedPsets.set(entityId, psetNames);
|
|
293
337
|
// Same rule as the attribute loop below: an overlay-CREATED entity is
|
|
294
338
|
// emitted once, by the new-entities pass, and already counted in
|
|
295
339
|
// `newEntityCount` — as are the pset entities this loop goes on to
|
|
296
340
|
// generate. Only the COUNT is guarded; the entity still records its
|
|
297
341
|
// pset edits and still emits them.
|
|
298
|
-
|
|
299
|
-
|
|
342
|
+
//
|
|
343
|
+
// A NOMINATION, in both modes, never a count on its own: this site sees
|
|
344
|
+
// a pset NAME the session touched, not whether that name resolves to
|
|
345
|
+
// anything. `deletePropertySet(id, 'AName')` on a host that owns no such
|
|
346
|
+
// set reaches here and changes nothing at all, and used to put "1
|
|
347
|
+
// modification" in the header of a byte-identical file (#2474). What
|
|
348
|
+
// settles it is the generator's `recordEmitted` and the skip branches'
|
|
349
|
+
// `recordWithheld` below.
|
|
350
|
+
if (!isOverlayCreated(entityId) && hasEmittableHostBytes(entityId)) {
|
|
351
|
+
modifications.nominate(entityId, 'property-set');
|
|
352
|
+
}
|
|
300
353
|
// Get the FULL mutated property sets for this entity (merged base + mutations)
|
|
301
354
|
const allPsets = this.mutationView.getForEntity(entityId);
|
|
302
355
|
const relevantPsets = allPsets.filter((pset) => psetNames.has(pset.name));
|
|
@@ -322,6 +375,12 @@ export class StepExporter {
|
|
|
322
375
|
for (const propId of propIds) {
|
|
323
376
|
skipPropertySetIds.add(propId);
|
|
324
377
|
}
|
|
378
|
+
// The other half of "did this edit change the file": a full export
|
|
379
|
+
// applies a set DELETION by leaving these lines out, and produces
|
|
380
|
+
// no replacement content to record an emission for. Without this
|
|
381
|
+
// the count would settle from the generator alone and a real
|
|
382
|
+
// deletion would stop counting along with the no-op one (#2474).
|
|
383
|
+
modifications.recordWithheld(entityId, 'property-set');
|
|
325
384
|
}
|
|
326
385
|
}
|
|
327
386
|
}
|
|
@@ -338,6 +397,12 @@ export class StepExporter {
|
|
|
338
397
|
for (const propId of propIds) {
|
|
339
398
|
skipPropertySetIds.add(propId);
|
|
340
399
|
}
|
|
400
|
+
// No `recordWithheld` twin of the rel-defined branch above, and
|
|
401
|
+
// deliberately: a name that matches an OWNED pset is either dropped
|
|
402
|
+
// from the resolved list or swapped for the replacement this export
|
|
403
|
+
// generated, so slot 5 always comes back different and the repoint
|
|
404
|
+
// below records the emission for it. A second record here would be
|
|
405
|
+
// one no mutation can kill.
|
|
341
406
|
}
|
|
342
407
|
for (const psetName of psetNames) {
|
|
343
408
|
if (!relDefinedPsetNames.has(psetName)) {
|
|
@@ -354,33 +419,134 @@ export class StepExporter {
|
|
|
354
419
|
// Collect modified quantity sets (only if quantities are included)
|
|
355
420
|
if (options.includeQuantities === false)
|
|
356
421
|
entityQuantMutations.clear();
|
|
422
|
+
// A quantity overlay with nothing under it regenerates a source quantity
|
|
423
|
+
// set from the edited quantity ALONE, and the skip loop below then
|
|
424
|
+
// withholds the source lines that held its siblings (#2487). Unlike
|
|
425
|
+
// properties — whose base falls back to the `baseTable` the view was
|
|
426
|
+
// constructed with — quantities have only the opt-in
|
|
427
|
+
// `setQuantityExtractor`, so the default really is an empty base, and
|
|
428
|
+
// four in-tree callers plus every external embedder never set it.
|
|
429
|
+
//
|
|
430
|
+
// The exporter is the one place that always holds the missing half: it
|
|
431
|
+
// was handed the very store the view is an overlay ON. Supplying it here
|
|
432
|
+
// makes the loss impossible for every caller rather than for the callers
|
|
433
|
+
// we happened to find, and a view that resolves its own quantities (the
|
|
434
|
+
// viewer, MCP, the CLI headless backend) is never overwritten.
|
|
435
|
+
//
|
|
436
|
+
// The extractor closes over ONE store, and the view outlives this export.
|
|
437
|
+
// So it closes over a BOX this class owns instead: a second export of the
|
|
438
|
+
// same view against a DIFFERENT store re-points that box rather than
|
|
439
|
+
// reading the first store's quantities, which is the one way "install only
|
|
440
|
+
// when absent" could have answered from the wrong file. The setter is
|
|
441
|
+
// called at most once per view, so a caller that installs its own
|
|
442
|
+
// extractor at any point — before the first export or after it — keeps it.
|
|
443
|
+
//
|
|
444
|
+
// `hasQuantityBase` and `setQuantityExtractor` are probed, like every other
|
|
445
|
+
// optional view capability this class reaches for (`peekNextExpressId`,
|
|
446
|
+
// `getNewEntities`, `getEntityTypeMutation`): `MutablePropertyView` is
|
|
447
|
+
// published API arriving from a separately versioned package, and callers
|
|
448
|
+
// pass partial and duck-typed views. `hasQuantityBase` is newer than
|
|
449
|
+
// `setQuantityExtractor`, and without it there is no way to tell an empty
|
|
450
|
+
// base from a caller-supplied one — so an older view falls back to the
|
|
451
|
+
// pre-#2487 behaviour (no base supplied) rather than risk overwriting one.
|
|
452
|
+
const quantityView = this.mutationView;
|
|
453
|
+
if (entityQuantMutations.size > 0 &&
|
|
454
|
+
typeof quantityView.setQuantityExtractor === 'function' &&
|
|
455
|
+
typeof quantityView.hasQuantityBase === 'function') {
|
|
456
|
+
const installed = exporterQuantityBase.get(quantityView);
|
|
457
|
+
if (installed) {
|
|
458
|
+
// Ours, or a caller's that replaced ours: re-pointing the box is a
|
|
459
|
+
// no-op in the second case, and calling the setter again is what
|
|
460
|
+
// would not be.
|
|
461
|
+
installed.store = this.dataStore;
|
|
462
|
+
}
|
|
463
|
+
else if (!quantityView.hasQuantityBase()) {
|
|
464
|
+
const box = { store: this.dataStore };
|
|
465
|
+
exporterQuantityBase.set(quantityView, box);
|
|
466
|
+
quantityView.setQuantityExtractor((id) => extractQuantitiesOnDemand(box.store, id));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
357
469
|
for (const [entityId, qsetNames] of entityQuantMutations) {
|
|
358
470
|
// Same rule as the property loop above: a deleted entity removes nothing.
|
|
359
471
|
if (effective.isDeleted(entityId))
|
|
360
472
|
continue;
|
|
361
473
|
modifiedEntities.add(entityId);
|
|
362
474
|
// See the property loop above — an overlay-created entity is counted as
|
|
363
|
-
// new, not modified.
|
|
364
|
-
|
|
365
|
-
|
|
475
|
+
// new, not modified. The pset loop's own nomination no longer has to be
|
|
476
|
+
// excluded to avoid a double count: the ledger settles per ENTITY, so a
|
|
477
|
+
// host with both a pset and a qset edit counts once whatever is
|
|
478
|
+
// nominated. Nominating both buys the opposite — an accurate warning
|
|
479
|
+
// when the qset half is the half a delta cannot carry.
|
|
480
|
+
//
|
|
481
|
+
// Settled from effect like its property-set twin (#2474). The reachable
|
|
482
|
+
// no-op here is an UNDONE quantity-set creation whose name matches NO
|
|
483
|
+
// source set: `getMutations()` is append-only, so the `CREATE_QUANTITY`
|
|
484
|
+
// record still names the qset after `removeQuantityMutation` has taken
|
|
485
|
+
// it out of the overlay, and the generator below then finds nothing to
|
|
486
|
+
// write. The same undo against a COLLIDING name is not a no-op — it
|
|
487
|
+
// withholds the source set's lines — which is what the skip loop's
|
|
488
|
+
// `recordWithheld` below settles.
|
|
489
|
+
if (!isOverlayCreated(entityId) && hasEmittableHostBytes(entityId)) {
|
|
490
|
+
modifications.nominate(entityId, 'quantity-set');
|
|
491
|
+
}
|
|
366
492
|
const allQsets = this.mutationView.getQuantitiesForEntity(entityId);
|
|
367
493
|
const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
|
|
368
494
|
if (relevantQsets.length > 0) {
|
|
369
495
|
newQuantitySets.push({ entityId, qsets: relevantQsets });
|
|
370
496
|
}
|
|
497
|
+
// The names this export is actually WRITING a replacement for. The
|
|
498
|
+
// affected-name set is not the same thing: it comes from the session's
|
|
499
|
+
// append-only mutation history, which keeps naming a quantity set after
|
|
500
|
+
// an undo has taken it back out of the overlay, so a Ctrl+Z used to
|
|
501
|
+
// withhold a source `IfcElementQuantity` that nothing regenerated. There
|
|
502
|
+
// is no quantity-set REMOVAL to preserve here — `deletedQsets` has no
|
|
503
|
+
// public populator, so withholding without a replacement is always the
|
|
504
|
+
// bug and never the intent (#2487).
|
|
505
|
+
const regeneratedQsetNames = new Set(relevantQsets.map((qset) => qset.name));
|
|
371
506
|
// Skip original quantity set entities (IfcElementQuantity).
|
|
372
507
|
// Same per-entity index lookup as the property branch above.
|
|
373
508
|
const rels = relDefinesByEntity.get(entityId);
|
|
374
509
|
if (rels) {
|
|
375
510
|
for (const { relId, psetId: relatedPsetId } of rels) {
|
|
376
511
|
const qsetName = this.getElementQuantityName(relatedPsetId);
|
|
377
|
-
if (qsetName &&
|
|
512
|
+
if (qsetName && regeneratedQsetNames.has(qsetName)) {
|
|
378
513
|
skipRelationshipIds.add(relId);
|
|
379
514
|
skipPropertySetIds.add(relatedPsetId);
|
|
380
515
|
const quantIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
381
516
|
for (const quantId of quantIds) {
|
|
382
517
|
skipPropertySetIds.add(quantId);
|
|
383
518
|
}
|
|
519
|
+
// The withheld half, exactly as the rel-defined property branch
|
|
520
|
+
// above. This loop has just decided that #`relatedPsetId`, its
|
|
521
|
+
// quantity atoms and the relationship that attached them do NOT
|
|
522
|
+
// go into the file; whether anything is generated to take their
|
|
523
|
+
// place is decided elsewhere, and is not this branch's to assume.
|
|
524
|
+
//
|
|
525
|
+
// It IS assumable for the pset side and not here, and the
|
|
526
|
+
// difference is where the two read their base from.
|
|
527
|
+
// `getForEntity` merges the overlay over the base pset walk, so a
|
|
528
|
+
// name the session touched but did not change still resolves to
|
|
529
|
+
// source content and is regenerated.
|
|
530
|
+
// `getQuantitiesForEntity` merges the overlay over
|
|
531
|
+
// `quantityExtractor`, which is OPT-IN: it defaults to null, and
|
|
532
|
+
// several in-tree callers wire the property extractor beside it
|
|
533
|
+
// and not it (`cli/commands/mutate.ts`, `gym.ts`,
|
|
534
|
+
// `generate-spaces.ts`, `export/demesh-session.ts`), as does any
|
|
535
|
+
// external embedder of these two published packages. With no
|
|
536
|
+
// extractor the base is empty and the overlay is the only source,
|
|
537
|
+
// so a qset the overlay no longer holds resolves to nothing.
|
|
538
|
+
//
|
|
539
|
+
// Which makes this reachable through an UNDONE quantity-set
|
|
540
|
+
// creation whose name COLLIDES with a source set:
|
|
541
|
+
// `setQuantity(id, 'Qto_WallBaseQuantities', ...)` followed by the
|
|
542
|
+
// `removeQuantityMutation` that mutationSlice runs on Ctrl+Z. The
|
|
543
|
+
// append-only history still names the qset, so this branch
|
|
544
|
+
// withholds the source lines; the overlay is empty again, so
|
|
545
|
+
// nothing is regenerated. The export drops the source quantity set
|
|
546
|
+
// — a real change to the file, and a data-loss bug of its own
|
|
547
|
+
// (#2487) — and this call is what stops the count from calling it
|
|
548
|
+
// nothing.
|
|
549
|
+
modifications.recordWithheld(entityId, 'quantity-set');
|
|
384
550
|
}
|
|
385
551
|
}
|
|
386
552
|
}
|
|
@@ -397,9 +563,29 @@ export class StepExporter {
|
|
|
397
563
|
// must not inflate the count either.
|
|
398
564
|
if (!hasEmittableHostBytes(entityId))
|
|
399
565
|
continue;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
566
|
+
// Under `deltaOnly` this only NOMINATES the host's ATTRIBUTE edits:
|
|
567
|
+
// nothing writes an in-place attribute edit into a delta except the
|
|
568
|
+
// type-object line rewrite, so the ledger drops it at settle time
|
|
569
|
+
// unless that pass reports having carried it (#2462). That nomination
|
|
570
|
+
// is deliberately made at INTENT: the per-kind warning exists to NAME
|
|
571
|
+
// an edit the delta could not carry, and an undeliverable edit is
|
|
572
|
+
// exactly the one that must still be named.
|
|
573
|
+
//
|
|
574
|
+
// A FULL export has no such warning, so an edit that resolved to
|
|
575
|
+
// nothing has nothing to say and nothing to claim — it waits for the
|
|
576
|
+
// rewrite instead. `setAttribute` to the value already in the slot, and
|
|
577
|
+
// `setAttribute` naming a slot the class does not declare, both leave
|
|
578
|
+
// the line byte-identical and used to count anyway (#2483).
|
|
579
|
+
//
|
|
580
|
+
// Recorded unconditionally. It used to be skipped for a host that also
|
|
581
|
+
// had a pset or qset edit, because the count was per entity and the
|
|
582
|
+
// other loop had already nominated it — which is exactly what let a
|
|
583
|
+
// pset emission mark the rename delivered and suppress its warning. The
|
|
584
|
+
// ledger de-duplicates the COUNT per entity now, so the two edits can
|
|
585
|
+
// and must be nominated separately.
|
|
586
|
+
inPlaceNominees.attribute.add(entityId);
|
|
587
|
+
if (options.deltaOnly === true)
|
|
588
|
+
modifications.nominate(entityId, 'attribute');
|
|
403
589
|
}
|
|
404
590
|
}
|
|
405
591
|
// Process georeferencing mutations (only when applyMutations is enabled)
|
|
@@ -455,10 +641,25 @@ export class StepExporter {
|
|
|
455
641
|
attrMap.set('MapUnit', `#${mapUnitRef}`);
|
|
456
642
|
changed = true;
|
|
457
643
|
}
|
|
458
|
-
if (changed
|
|
644
|
+
if (changed) {
|
|
459
645
|
modifiedEntities.add(entityId);
|
|
460
|
-
|
|
461
|
-
|
|
646
|
+
// Queued as attribute edits, which only the source-iteration pass
|
|
647
|
+
// writes — so under `deltaOnly` this nominates and settle decides.
|
|
648
|
+
// Recorded even when the host is already in `modifiedEntities`: that
|
|
649
|
+
// guard existed to stop a second COUNT, which the ledger now handles
|
|
650
|
+
// per entity, and suppressing the nomination would hide a dropped
|
|
651
|
+
// georeferencing edit behind an unrelated edit to the same record.
|
|
652
|
+
//
|
|
653
|
+
// `changed` above is INTENT — a field was supplied, not a field that
|
|
654
|
+
// differs from the one in the file. Writing `name: 'EPSG:2056'` over
|
|
655
|
+
// an IfcProjectedCRS already named `EPSG:2056` leaves the line
|
|
656
|
+
// byte-identical, so a full export waits for the rewrite exactly as
|
|
657
|
+
// the plain attribute site does (#2483).
|
|
658
|
+
if (hasEmittableHostBytes(entityId)) {
|
|
659
|
+
inPlaceNominees.georeferencing.add(entityId);
|
|
660
|
+
if (options.deltaOnly === true)
|
|
661
|
+
modifications.nominate(entityId, 'georeferencing');
|
|
662
|
+
}
|
|
462
663
|
}
|
|
463
664
|
}
|
|
464
665
|
// Modify existing IfcMapConversion
|
|
@@ -494,10 +695,14 @@ export class StepExporter {
|
|
|
494
695
|
attrMap.set('Scale', String(mc.scale));
|
|
495
696
|
changed = true;
|
|
496
697
|
}
|
|
497
|
-
if (changed
|
|
698
|
+
if (changed) {
|
|
498
699
|
modifiedEntities.add(entityId);
|
|
499
|
-
|
|
500
|
-
|
|
700
|
+
// Same as the IfcProjectedCRS branch above, effect gate included.
|
|
701
|
+
if (hasEmittableHostBytes(entityId)) {
|
|
702
|
+
inPlaceNominees.georeferencing.add(entityId);
|
|
703
|
+
if (options.deltaOnly === true)
|
|
704
|
+
modifications.nominate(entityId, 'georeferencing');
|
|
705
|
+
}
|
|
501
706
|
}
|
|
502
707
|
}
|
|
503
708
|
// CREATE new georef entities when file has none
|
|
@@ -635,7 +840,10 @@ export class StepExporter {
|
|
|
635
840
|
// deltaOnly carve-out the source branch gets.
|
|
636
841
|
return !isGeometryExcluded(entityId, ref.type);
|
|
637
842
|
}
|
|
638
|
-
|
|
843
|
+
// Same readability test as `hasEmittableHostBytes`, and for the reason
|
|
844
|
+
// that predicate names: a ref this source cannot address is not a line
|
|
845
|
+
// this export can write, so nothing may be generated naming it (#2491).
|
|
846
|
+
if (!isReadableSourceRef(ref))
|
|
639
847
|
return false;
|
|
640
848
|
// Mirrors `hasEmittableHostBytes`: under `deltaOnly` the source-
|
|
641
849
|
// iteration pass — and its geometry skip — never runs, so a source
|
|
@@ -657,8 +865,12 @@ export class StepExporter {
|
|
|
657
865
|
// dropped everything the overlay tombstoned, so there is no separate
|
|
658
866
|
// deleted check to forget here.
|
|
659
867
|
for (const [expressId, entityRef] of effective) {
|
|
660
|
-
// Skip overlay-only entities — emitted by the new-entities pass below
|
|
661
|
-
|
|
868
|
+
// Skip overlay-only entities — emitted by the new-entities pass below.
|
|
869
|
+
// A ref this source cannot address is skipped by the same test rather
|
|
870
|
+
// than decoded: `decodeUtf8` clamps such a range and the empty string
|
|
871
|
+
// it returns used to be pushed into the file as a blank line, leaving
|
|
872
|
+
// every generated record that names the host dangling (#2491).
|
|
873
|
+
if (!isReadableSourceRef(entityRef)) {
|
|
662
874
|
continue;
|
|
663
875
|
}
|
|
664
876
|
// Skip entities outside the visible closure
|
|
@@ -684,42 +896,29 @@ export class StepExporter {
|
|
|
684
896
|
// SharedArrayBuffer-backed view; the parser deliberately keeps
|
|
685
897
|
// `source` zero-copy SAB-backed for worker sharing).
|
|
686
898
|
const entityText = decodeRange(source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (modifiedAttributes.has(expressId)) {
|
|
711
|
-
nextEntityText = this.applyAttributeMutations(nextEntityText, workingType, modifiedAttributes.get(expressId));
|
|
712
|
-
}
|
|
713
|
-
const positional = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
714
|
-
? this.mutationView.getPositionalMutationsForEntity(expressId)
|
|
715
|
-
: null;
|
|
716
|
-
if (positional && positional.size > 0) {
|
|
717
|
-
nextEntityText = this.applyPositionalMutations(nextEntityText, positional, workingType, sourceSchema);
|
|
718
|
-
if (!modifiedEntities.has(expressId)) {
|
|
719
|
-
modifiedEntities.add(expressId);
|
|
720
|
-
modifiedEntityCount++;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
899
|
+
// Retype, named attribute edits and positional edits, in that order.
|
|
900
|
+
// Shared verbatim with the type-object `HasPropertySets` rewrite below,
|
|
901
|
+
// which writes the line this pass would otherwise have written.
|
|
902
|
+
const mutated = this.applySourceLineMutations(expressId, entityText, entityRef.type, modifiedAttributes.get(expressId), sourceSchema, overlayActive);
|
|
903
|
+
const nextEntityText = mutated.text;
|
|
904
|
+
// A retype or a positional edit that CHANGED the line is what makes
|
|
905
|
+
// this entity count; a named attribute edit was already nominated by
|
|
906
|
+
// the collection pass. Both flags report effect, so retyping an entity
|
|
907
|
+
// to the class it already is — or writing a slot the token it already
|
|
908
|
+
// holds — no longer claims a modification over a line the export left
|
|
909
|
+
// byte-identical. This pass is full-export-only (`deltaOnly` skips it
|
|
910
|
+
// wholesale), so nomination IS emission here and the kinds only have to
|
|
911
|
+
// be right for the entity count — which is per entity, hence unchanged.
|
|
912
|
+
if (mutated.retyped || mutated.positional)
|
|
913
|
+
modifiedEntities.add(expressId);
|
|
914
|
+
if (mutated.retyped)
|
|
915
|
+
modifications.nominate(expressId, 'retype');
|
|
916
|
+
if (mutated.positional)
|
|
917
|
+
modifications.nominate(expressId, 'positional');
|
|
918
|
+
// The named-attribute kinds join them here rather than at their
|
|
919
|
+
// collection sites, for the same reason and on the same signal (#2483).
|
|
920
|
+
// This pass is full-export-only, so there is nothing to gate.
|
|
921
|
+
nominateDeliveredInPlaceEdits(modifications, expressId, mutated, inPlaceNominees);
|
|
723
922
|
// Apply schema conversion if exporting to a different schema version
|
|
724
923
|
if (converting) {
|
|
725
924
|
const converted = convertStepLine(nextEntityText, sourceSchema, schema, options.guidRandom);
|
|
@@ -743,6 +942,10 @@ export class StepExporter {
|
|
|
743
942
|
const newEntities = this.generatePropertySetEntities(entityId, psets, willBeEmitted, effective, typeOwnedPsetNamesByEntity.get(entityId), options.guidRandom);
|
|
744
943
|
entities.push(...newEntities.lines);
|
|
745
944
|
newEntityCount += newEntities.count;
|
|
945
|
+
// Replacement content for this host actually landed, so a delta really
|
|
946
|
+
// does carry its PROPERTY-SET modification — and only that one (#2462).
|
|
947
|
+
if (newEntities.lines.length > 0)
|
|
948
|
+
modifications.recordEmitted(entityId, 'property-set');
|
|
746
949
|
generatedTypeOwnedPsetIds.set(entityId, newEntities.generatedTypeOwnedPsetIds);
|
|
747
950
|
}
|
|
748
951
|
// Point every affected type object's HasPropertySets at the psets this
|
|
@@ -761,9 +964,108 @@ export class StepExporter {
|
|
|
761
964
|
overlayTypeOwnedPsets.set(entityId, resolved.length > 0 ? resolved.map((id) => `#${id}`) : null);
|
|
762
965
|
continue;
|
|
763
966
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
967
|
+
// This line REPLACES the one the source-iteration pass would have
|
|
968
|
+
// written — `rewrittenEntityIds` makes that pass skip the entity — so it
|
|
969
|
+
// has to carry the entity's other edits too, and it has to apply them
|
|
970
|
+
// the way that pass does. It used to replace slot 5 and nothing else,
|
|
971
|
+
// which dropped the rename in `setAttribute(id,'Name',…)` +
|
|
972
|
+
// `addPropertySet(id,…)`, and then, once renames were special-cased
|
|
973
|
+
// here, still dropped retypes and positional edits — same line, same
|
|
974
|
+
// silence. So run the ONE pipeline both passes share and replace
|
|
975
|
+
// `HasPropertySets` on its output. Order matters: see
|
|
976
|
+
// {@link applySourceLineMutations}.
|
|
977
|
+
const record = effective.get(entityId);
|
|
978
|
+
let sourceLine = null;
|
|
979
|
+
let mutated = null;
|
|
980
|
+
// One narrowed block for both calls: `record` is in scope for the decode
|
|
981
|
+
// AND for the record type below, with no non-null assertion to keep true
|
|
982
|
+
// by hand. `byteOffset >= 0` is the same "are there real source bytes"
|
|
983
|
+
// test the source-iteration pass makes — an overlay-authored record
|
|
984
|
+
// carries `-1` there, and decoding from it would read another entity's
|
|
985
|
+
// bytes rather than fall through to the no-source-bytes branch.
|
|
986
|
+
// `isReadableSourceRef` folds in the `byteOffset >= 0 && byteLength > 0`
|
|
987
|
+
// test this used to make by hand, and adds the bound the invariant used
|
|
988
|
+
// to supply (#2491).
|
|
989
|
+
if (record && isReadableSourceRef(record)) {
|
|
990
|
+
sourceLine = decodeRange(this.dataStore.source, record.byteOffset, record.byteOffset + record.byteLength);
|
|
991
|
+
// The RECORD's class is the from-type: the bytes are still the source
|
|
992
|
+
// class, whatever `typeOf` now says the entity effectively is.
|
|
993
|
+
mutated = this.applySourceLineMutations(entityId, sourceLine, record.type, modifiedAttributes.get(entityId), sourceSchema, overlayActive);
|
|
994
|
+
}
|
|
995
|
+
if (mutated === null) {
|
|
996
|
+
// `willBeEmitted` already required real source bytes for a non-overlay
|
|
997
|
+
// record, so this is only reachable with no source buffer at all —
|
|
998
|
+
// in which case the source-iteration pass never ran either and there is
|
|
999
|
+
// nothing to lose. Say it anyway; the pset edit is still going nowhere.
|
|
1000
|
+
warnings.push(typeOwnedPsetRewriteWarning(entityId, 'no-source-bytes'));
|
|
1001
|
+
// The line above IS the report, so the ledger must not add a second,
|
|
1002
|
+
// vaguer one blaming the delta format for a drop the format did not
|
|
1003
|
+
// cause.
|
|
1004
|
+
modifications.acknowledgeUndelivered(entityId, 'property-set');
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const { line, repointed } = rewriteTypeOwnedPsetLine(mutated.text, resolved);
|
|
1008
|
+
if (repointed) {
|
|
1009
|
+
// A repoint that resolves to the list the line ALREADY names changes
|
|
1010
|
+
// nothing, and it is reachable: deleting a pset name the type object
|
|
1011
|
+
// does not own leaves every original id in place (it is "affected" but
|
|
1012
|
+
// matches none of them) and generates no replacement, so slot 5 comes
|
|
1013
|
+
// back byte-identical. Same rule as the fallback branch below — an
|
|
1014
|
+
// unchanged line has no place in a delta, and claiming it delivered the
|
|
1015
|
+
// edit would put a modification in the header over a line that carries
|
|
1016
|
+
// none. A FULL export still emits it: `rewrittenEntityIds` made the
|
|
1017
|
+
// source-iteration pass skip this entity, so withholding the line there
|
|
1018
|
+
// would delete the record from the file (#2469).
|
|
1019
|
+
const changed = line !== sourceLine;
|
|
1020
|
+
if (options.deltaOnly !== true || changed) {
|
|
1021
|
+
rewrittenEntityLines.set(entityId, line);
|
|
1022
|
+
}
|
|
1023
|
+
// A rewritten source line IS in the delta — the one in-place change a
|
|
1024
|
+
// delta does carry today (#2462). The repoint itself delivers the
|
|
1025
|
+
// property-set edit that put this host in the loop; the rest of the
|
|
1026
|
+
// line delivers whichever in-place edits the pipeline applied to it.
|
|
1027
|
+
if (changed) {
|
|
1028
|
+
modifications.recordEmitted(entityId, 'property-set');
|
|
1029
|
+
recordSourceLineDelivery(modifications, entityId, mutated);
|
|
1030
|
+
// `rewrittenEntityIds` made the source-iteration pass skip this
|
|
1031
|
+
// host, so this line is the ONLY place a full export can see its
|
|
1032
|
+
// named-attribute edits land — per site, not per feature (#2483).
|
|
1033
|
+
nominateDeliveredInPlaceEdits(modifications, entityId, mutated, inPlaceNominees);
|
|
1034
|
+
}
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
// A malformed source line — too few arguments to have a slot 5, or not
|
|
1038
|
+
// parseable as a STEP record at all. The entity must still come out:
|
|
1039
|
+
// `rewrittenEntityIds` made the source-iteration pass skip it, so
|
|
1040
|
+
// dropping the line here deletes the whole record from the file (#2469).
|
|
1041
|
+
warnings.push(typeOwnedPsetRewriteWarning(entityId, 'unparseable-line'));
|
|
1042
|
+
// Same as the `no-source-bytes` branch: the property-set edit is
|
|
1043
|
+
// genuinely undelivered — the repoint is what would have delivered it and
|
|
1044
|
+
// it did not happen — but this warning already says so, precisely, so the
|
|
1045
|
+
// ledger stays quiet about that pair rather than duplicating it. (When
|
|
1046
|
+
// the affected psets produced replacement content, the property-set pass
|
|
1047
|
+
// above has already recorded the emission, and an emission outranks an
|
|
1048
|
+
// acknowledgement.)
|
|
1049
|
+
modifications.acknowledgeUndelivered(entityId, 'property-set');
|
|
1050
|
+
// `line` is byte-for-byte what the source-iteration pass would have
|
|
1051
|
+
// written, so emit it wherever that pass would have run. Under
|
|
1052
|
+
// `deltaOnly` it does not run, and a line the mutation pipeline left
|
|
1053
|
+
// identical to its source is not a change — it has no place in a delta.
|
|
1054
|
+
const changed = line !== sourceLine;
|
|
1055
|
+
if (options.deltaOnly !== true || changed) {
|
|
1056
|
+
rewrittenEntityLines.set(entityId, line);
|
|
1057
|
+
}
|
|
1058
|
+
// The ledger stays honest about WHICH modification landed: the
|
|
1059
|
+
// property-set edit that nominated this host is the thing that just
|
|
1060
|
+
// failed, so only the entity's OTHER edits are in this line. Under the
|
|
1061
|
+
// per-kind keying that comes out as `attribute/retype/positional:
|
|
1062
|
+
// delivered, property-set: undelivered` — the host still counts once,
|
|
1063
|
+
// because a real change of its did land.
|
|
1064
|
+
if (changed) {
|
|
1065
|
+
recordSourceLineDelivery(modifications, entityId, mutated);
|
|
1066
|
+
// Same site rule as the repoint branch above: the failed repoint is
|
|
1067
|
+
// what did not land, and the line still carries the host's OTHER edits.
|
|
1068
|
+
nominateDeliveredInPlaceEdits(modifications, entityId, mutated, inPlaceNominees);
|
|
767
1069
|
}
|
|
768
1070
|
}
|
|
769
1071
|
// Generate new quantity entities for mutations
|
|
@@ -773,6 +1075,8 @@ export class StepExporter {
|
|
|
773
1075
|
const newEntities = this.generateQuantitySetEntities(entityId, qsets, willBeEmitted, options.guidRandom);
|
|
774
1076
|
entities.push(...newEntities.lines);
|
|
775
1077
|
newEntityCount += newEntities.count;
|
|
1078
|
+
if (newEntities.lines.length > 0)
|
|
1079
|
+
modifications.recordEmitted(entityId, 'quantity-set');
|
|
776
1080
|
}
|
|
777
1081
|
for (const rewrittenLine of rewrittenEntityLines.values()) {
|
|
778
1082
|
entities.push(rewrittenLine);
|
|
@@ -863,6 +1167,13 @@ export class StepExporter {
|
|
|
863
1167
|
}
|
|
864
1168
|
}
|
|
865
1169
|
}
|
|
1170
|
+
// Settle the count against what the passes above actually wrote, and say
|
|
1171
|
+
// out loud every KIND of edit a delta could not carry, per host. Silence
|
|
1172
|
+
// was the other half of #2462: `deltaOnly` skips the source-iteration pass,
|
|
1173
|
+
// so an in-place edit to a source entity is not in the file and never was —
|
|
1174
|
+
// the header merely used to claim otherwise.
|
|
1175
|
+
const { modifiedEntityCount, warnings: deltaWarnings } = modifications.settle();
|
|
1176
|
+
warnings.push(...deltaWarnings);
|
|
866
1177
|
// Assemble final file as Uint8Array chunks to avoid V8 string length limit.
|
|
867
1178
|
// The header is built last so its provenance item reflects the real count.
|
|
868
1179
|
const header = buildHeader(newEntityCount + modifiedEntityCount);
|
|
@@ -989,7 +1300,10 @@ export class StepExporter {
|
|
|
989
1300
|
return result;
|
|
990
1301
|
}
|
|
991
1302
|
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
992
|
-
|
|
1303
|
+
// Readability rather than presence, as everywhere else (#2491). A clamped
|
|
1304
|
+
// decode would match nothing here, so this is tidiness rather than a bug —
|
|
1305
|
+
// but the gates in this file agree on one predicate now.
|
|
1306
|
+
if (entityRef && createSourceRefReader(this.dataStore.source)(entityRef)) {
|
|
993
1307
|
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
994
1308
|
// #ID=IFCWALL('GlobalId',#owner,...): GlobalId is a quoted STEP string
|
|
995
1309
|
// (doubled '' escapes); OwnerHistory is the ref/`$` right after it.
|
|
@@ -1013,7 +1327,12 @@ export class StepExporter {
|
|
|
1013
1327
|
for (const prop of pset.properties) {
|
|
1014
1328
|
const propId = this.nextExpressId++;
|
|
1015
1329
|
count++;
|
|
1016
|
-
|
|
1330
|
+
// `prop.dataType`, not `prop.type` alone: regenerating the set rewrites
|
|
1331
|
+
// every property in it, and the shape-derived primitive would re-declare
|
|
1332
|
+
// the ones nobody edited (`IFCTEXT` → `IFCLABEL`, `IFCLENGTHMEASURE` →
|
|
1333
|
+
// `IFCREAL`). See `declared-property-type.ts` for when the source token
|
|
1334
|
+
// is trusted (#2482).
|
|
1335
|
+
const valueStr = serializeNominalValue(prop.value, prop.type, prop.dataType);
|
|
1017
1336
|
const unitId = prop.unit ? this.findUnitId(prop.unit, effective) : null;
|
|
1018
1337
|
const unitStr = unitId !== null ? ref(unitId) : null;
|
|
1019
1338
|
// #ID=IFCPROPERTYSINGLEVALUE('Name',$,Value,Unit);
|
|
@@ -1079,6 +1398,86 @@ export class StepExporter {
|
|
|
1079
1398
|
}
|
|
1080
1399
|
return { lines, count };
|
|
1081
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* THE mutation pipeline for a line read out of the source buffer: retype,
|
|
1403
|
+
* then named attribute edits, then positional edits.
|
|
1404
|
+
*
|
|
1405
|
+
* **One implementation, two call sites**, and that is the whole point. Two
|
|
1406
|
+
* passes can write the defining line of a source entity — the
|
|
1407
|
+
* source-iteration pass, and the type-object `HasPropertySets` rewrite that
|
|
1408
|
+
* REPLACES it (`rewrittenEntityIds` makes the source pass skip those ids).
|
|
1409
|
+
* The rewrite used to do its own thing (replace slot 5, nothing else), so
|
|
1410
|
+
* every other edit to a type object with a type-owned pset edit was dropped
|
|
1411
|
+
* in silence: first the renames (#2462 follow-up), and after those were
|
|
1412
|
+
* special-cased here, still the retypes and the positional edits. Whatever
|
|
1413
|
+
* the source pass applies, the rewrite has to apply too, or the next edit
|
|
1414
|
+
* kind added to one site goes missing at the other.
|
|
1415
|
+
*
|
|
1416
|
+
* The order is load-bearing:
|
|
1417
|
+
*
|
|
1418
|
+
* - the retype runs FIRST so named attribute edits resolve against the
|
|
1419
|
+
* TARGET class's attribute names, and so positional slots are indexed
|
|
1420
|
+
* into the retyped argument list;
|
|
1421
|
+
* - the `HasPropertySets` replacement (rewrite path only) runs LAST, on
|
|
1422
|
+
* the text this returns. Run it first and a positional edit to slot 5 —
|
|
1423
|
+
* or a retype's argument-list rebuild — overwrites the resolved pset
|
|
1424
|
+
* list with the stale one, which is the same silent drop one slot over.
|
|
1425
|
+
*
|
|
1426
|
+
* The expressId is unchanged by all of this, so geometry / placement /
|
|
1427
|
+
* representation and every IfcRel* reference (keyed by #id) carry over.
|
|
1428
|
+
*
|
|
1429
|
+
* All three flags report EFFECT, not intent — each is the answer to "did this
|
|
1430
|
+
* operation change the line", measured across that operation alone. The count
|
|
1431
|
+
* and the ledger are claims about the FILE, so an edit that resolves to the
|
|
1432
|
+
* text already there has delivered nothing and must not be reported: retyping
|
|
1433
|
+
* an entity to the class it already is, or writing a positional slot the token
|
|
1434
|
+
* it already holds, used to count as a modification and reach the ledger as a
|
|
1435
|
+
* landed edit, over a byte-identical line. Discarded edits read the same way:
|
|
1436
|
+
* `applyAttributeMutations` drops a name its class has no slot for and
|
|
1437
|
+
* `retypeStepLine` returns an unparseable line untouched, and neither is a
|
|
1438
|
+
* modification of anything.
|
|
1439
|
+
*
|
|
1440
|
+
* `retyped` / `positional` matter most in a FULL export, which is where the
|
|
1441
|
+
* two are nominated (their edits have no earlier nomination site); named
|
|
1442
|
+
* attribute edits are nominated by the collection pass and `attributed` only
|
|
1443
|
+
* settles their delivery.
|
|
1444
|
+
*/
|
|
1445
|
+
applySourceLineMutations(expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive) {
|
|
1446
|
+
let text = entityText;
|
|
1447
|
+
let workingType = recordType.toUpperCase();
|
|
1448
|
+
const typeMutation = overlayActive && typeof this.mutationView.getEntityTypeMutation === 'function'
|
|
1449
|
+
? this.mutationView.getEntityTypeMutation(expressId)
|
|
1450
|
+
: null;
|
|
1451
|
+
let retyped = false;
|
|
1452
|
+
if (typeMutation) {
|
|
1453
|
+
const beforeRetype = text;
|
|
1454
|
+
text = retypeStepLine(text, recordType, typeMutation.newType, typeMutation.predefinedType ?? null, sourceSchema);
|
|
1455
|
+
retyped = text !== beforeRetype;
|
|
1456
|
+
// Set even for a no-op retype: the entity IS the target class from here
|
|
1457
|
+
// on, so the named and positional edits below must resolve against it.
|
|
1458
|
+
workingType = typeMutation.newType.toUpperCase();
|
|
1459
|
+
}
|
|
1460
|
+
// `applyAttributeMutations` returns its input UNCHANGED when it wrote
|
|
1461
|
+
// nothing — no slot resolved for any of the names, or the line does not
|
|
1462
|
+
// parse — so comparing is what tells the ledger whether a named attribute
|
|
1463
|
+
// edit was really carried, rather than merely attempted.
|
|
1464
|
+
let attributed = false;
|
|
1465
|
+
if (attributeMutations && attributeMutations.size > 0) {
|
|
1466
|
+
const beforeAttributes = text;
|
|
1467
|
+
text = this.applyAttributeMutations(text, workingType, attributeMutations);
|
|
1468
|
+
attributed = text !== beforeAttributes;
|
|
1469
|
+
}
|
|
1470
|
+
const positionals = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
1471
|
+
? this.mutationView.getPositionalMutationsForEntity(expressId)
|
|
1472
|
+
: null;
|
|
1473
|
+
let positional = false;
|
|
1474
|
+
if (positionals && positionals.size > 0) {
|
|
1475
|
+
const beforePositionals = text;
|
|
1476
|
+
text = this.applyPositionalMutations(text, positionals, workingType, sourceSchema);
|
|
1477
|
+
positional = text !== beforePositionals;
|
|
1478
|
+
}
|
|
1479
|
+
return { text, attributed, retyped, positional };
|
|
1480
|
+
}
|
|
1082
1481
|
/**
|
|
1083
1482
|
* Rewrite root IFC attributes directly on the original STEP entity line.
|
|
1084
1483
|
*/
|
|
@@ -1617,24 +2016,6 @@ export class StepExporter {
|
|
|
1617
2016
|
return [];
|
|
1618
2017
|
return hasPropertySets.filter((value) => typeof value === 'number');
|
|
1619
2018
|
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Replace a single top-level STEP attribute in an entity line.
|
|
1622
|
-
*/
|
|
1623
|
-
replaceEntityAttribute(entityId, attrIndex, replacement) {
|
|
1624
|
-
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1625
|
-
if (!entityRef || !this.dataStore.source)
|
|
1626
|
-
return null;
|
|
1627
|
-
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1628
|
-
const match = entityText.match(/^(#\d+\s*=\s*\w+\()([\s\S]*)(\)\s*;)\s*$/);
|
|
1629
|
-
if (!match)
|
|
1630
|
-
return null;
|
|
1631
|
-
const [, prefix, attrsText, suffix] = match;
|
|
1632
|
-
const attrs = splitTopLevelStepArguments(attrsText);
|
|
1633
|
-
if (attrIndex >= attrs.length)
|
|
1634
|
-
return null;
|
|
1635
|
-
attrs[attrIndex] = replacement;
|
|
1636
|
-
return `${prefix}${attrs.join(',')}${suffix}`;
|
|
1637
|
-
}
|
|
1638
2019
|
}
|
|
1639
2020
|
/**
|
|
1640
2021
|
* Quick export function for simple use cases.
|