@ifc-lite/export 2.9.3 → 3.0.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/csv-cell.d.ts +101 -0
- package/dist/csv-cell.d.ts.map +1 -0
- package/dist/csv-cell.js +90 -0
- package/dist/csv-cell.js.map +1 -0
- package/dist/demesh-session.d.ts.map +1 -1
- package/dist/demesh-session.js +2 -1
- package/dist/demesh-session.js.map +1 -1
- package/dist/effective-index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/lod1-generator.d.ts +0 -2
- package/dist/lod1-generator.d.ts.map +1 -1
- package/dist/lod1-generator.js +1 -1
- package/dist/lod1-generator.js.map +1 -1
- package/dist/merged-exporter.d.ts +3 -2
- package/dist/merged-exporter.d.ts.map +1 -1
- package/dist/merged-exporter.js +32 -44
- package/dist/merged-exporter.js.map +1 -1
- package/dist/parquet-exporter.js +2 -0
- package/dist/parquet-exporter.js.map +1 -1
- package/dist/reference-collector.d.ts +40 -17
- package/dist/reference-collector.d.ts.map +1 -1
- package/dist/reference-collector.js +89 -19
- package/dist/reference-collector.js.map +1 -1
- package/dist/schema-converter.d.ts.map +1 -1
- package/dist/schema-converter.js +27 -1
- package/dist/schema-converter.js.map +1 -1
- package/dist/source-ref-bounds.d.ts +16 -1
- package/dist/source-ref-bounds.d.ts.map +1 -1
- package/dist/source-ref-bounds.js +85 -0
- package/dist/source-ref-bounds.js.map +1 -1
- package/dist/step-attribute-mutations.d.ts +91 -0
- package/dist/step-attribute-mutations.d.ts.map +1 -0
- package/dist/step-attribute-mutations.js +302 -0
- package/dist/step-attribute-mutations.js.map +1 -0
- package/dist/step-collection.d.ts +55 -0
- package/dist/step-collection.d.ts.map +1 -0
- package/dist/step-collection.js +125 -0
- package/dist/step-collection.js.map +1 -0
- package/dist/step-export-contexts.d.ts +63 -0
- package/dist/step-export-contexts.d.ts.map +1 -0
- package/dist/step-export-contexts.js +51 -0
- package/dist/step-export-contexts.js.map +1 -0
- package/dist/step-export-types.d.ts +262 -0
- package/dist/step-export-types.d.ts.map +1 -0
- package/dist/step-export-types.js +22 -0
- package/dist/step-export-types.js.map +1 -0
- package/dist/step-exporter.d.ts +45 -398
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +145 -2033
- package/dist/step-exporter.js.map +1 -1
- package/dist/step-geometry-types.d.ts +19 -0
- package/dist/step-geometry-types.d.ts.map +1 -0
- package/dist/step-geometry-types.js +57 -0
- package/dist/step-geometry-types.js.map +1 -0
- package/dist/step-georeferencing.d.ts +58 -0
- package/dist/step-georeferencing.d.ts.map +1 -0
- package/dist/step-georeferencing.js +339 -0
- package/dist/step-georeferencing.js.map +1 -0
- package/dist/step-header.d.ts +43 -0
- package/dist/step-header.d.ts.map +1 -0
- package/dist/step-header.js +70 -0
- package/dist/step-header.js.map +1 -0
- package/dist/step-omission-predicates.d.ts +51 -0
- package/dist/step-omission-predicates.d.ts.map +1 -0
- package/dist/step-omission-predicates.js +268 -0
- package/dist/step-omission-predicates.js.map +1 -0
- package/dist/step-overlay-entities.d.ts +64 -0
- package/dist/step-overlay-entities.d.ts.map +1 -0
- package/dist/step-overlay-entities.js +119 -0
- package/dist/step-overlay-entities.js.map +1 -0
- package/dist/step-pass-builder.d.ts +44 -0
- package/dist/step-pass-builder.d.ts.map +1 -0
- package/dist/step-pass-builder.js +249 -0
- package/dist/step-pass-builder.js.map +1 -0
- package/dist/step-property-sets.d.ts +198 -0
- package/dist/step-property-sets.d.ts.map +1 -0
- package/dist/step-property-sets.js +842 -0
- package/dist/step-property-sets.js.map +1 -0
- package/dist/step-serialization.d.ts +16 -1
- package/dist/step-serialization.d.ts.map +1 -1
- package/dist/step-serialization.js +50 -3
- package/dist/step-serialization.js.map +1 -1
- package/dist/step-source-iteration.d.ts +95 -0
- package/dist/step-source-iteration.d.ts.map +1 -0
- package/dist/step-source-iteration.js +178 -0
- package/dist/step-source-iteration.js.map +1 -0
- package/package.json +7 -7
package/dist/step-exporter.js
CHANGED
|
@@ -1,65 +1,20 @@
|
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import { collectReferencedEntityIds, getVisibleEntityIds, collectStyleEntities, filterHiddenRefsFromRelationshipLine, } from './reference-collector.js';
|
|
7
|
-
import { convertStepLine, needsConversion } from './schema-converter.js';
|
|
8
|
-
import { retypeStepLine, retypeArgTokens } from './retype.js';
|
|
4
|
+
import { EntityExtractor, parseSourceHeader } from '@ifc-lite/parser';
|
|
5
|
+
import { needsConversion } from './schema-converter.js';
|
|
9
6
|
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
7
|
import { createSourceRefReader } from './source-ref-bounds.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
* UTF-8 decode of `[start, end)` of the source, accepting either the raw bytes
|
|
24
|
-
* or the {@link IfcSourceBytes} accessor (#2183). Replaces the direct
|
|
25
|
-
* `safeUtf8Decode(source, …)` calls this file used to make: `decodeUtf8` is
|
|
26
|
-
* SAB-safe in exactly the same way, and routing through the accessor is what
|
|
27
|
-
* lets `IfcDataStore.source` change shape without touching these four reads.
|
|
28
|
-
*/
|
|
29
|
-
function decodeRange(src, start, end) {
|
|
30
|
-
return asSourceBytes(src).decodeUtf8(start, end);
|
|
31
|
-
}
|
|
32
|
-
/** `OwnerHistory` is slot 1 on every `IfcRoot` subtype, all schemas. */
|
|
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();
|
|
49
|
-
/**
|
|
50
|
-
* Message for the one refusal `export()` can report, shared by the returned
|
|
51
|
-
* `stats.warnings` entry and the console line so the two cannot drift.
|
|
52
|
-
*/
|
|
53
|
-
const MAP_CONVERSION_WITHOUT_CONTEXT_WARNING = 'Cannot create IfcMapConversion: no IfcGeometricRepresentationContext is available to reference as SourceCRS. The IfcProjectedCRS is unaffected.';
|
|
54
|
-
/**
|
|
55
|
-
* Message for the refusal `export()` reports when a map conversion is
|
|
56
|
-
* requested but there is no IfcProjectedCRS to attach it to — none was
|
|
57
|
-
* requested and none exists in the file — distinct from
|
|
58
|
-
* {@link MAP_CONVERSION_WITHOUT_CONTEXT_WARNING}, which is worded for the
|
|
59
|
-
* case where an IfcProjectedCRS exists (or was written) but no context is
|
|
60
|
-
* available to reference.
|
|
61
|
-
*/
|
|
62
|
-
const MAP_CONVERSION_WITHOUT_CRS_WARNING = 'Cannot create IfcMapConversion: no IfcProjectedCRS was requested and none exists in the file to reference as TargetCRS. Nothing was written.';
|
|
8
|
+
import { writeSourceEntityLines } from './step-source-iteration.js';
|
|
9
|
+
import { writeOverlayCreatedEntities } from './step-overlay-entities.js';
|
|
10
|
+
import { generatePropertyAndQuantitySetEntities, } from './step-property-sets.js';
|
|
11
|
+
import { collectModifications } from './step-collection.js';
|
|
12
|
+
import { assembleExportResult } from './step-header.js';
|
|
13
|
+
import { applySourceLineMutations } from './step-attribute-mutations.js';
|
|
14
|
+
import { buildExportPass } from './step-pass-builder.js';
|
|
15
|
+
import { evaluateOmissionPredicates } from './step-omission-predicates.js';
|
|
16
|
+
import { isGeometryEntity } from './step-geometry-types.js';
|
|
17
|
+
import { buildSourceIterationContext, buildOverlayEntitiesContext, } from './step-export-contexts.js';
|
|
63
18
|
/**
|
|
64
19
|
* IFC STEP file exporter
|
|
65
20
|
*/
|
|
@@ -68,11 +23,16 @@ export class StepExporter {
|
|
|
68
23
|
mutationView;
|
|
69
24
|
nextExpressId;
|
|
70
25
|
entityExtractor;
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The owner-history memos the property-set and quantity-set generators read.
|
|
28
|
+
*
|
|
29
|
+
* Owned here and handed to `step-property-sets.ts` BY REFERENCE rather than
|
|
30
|
+
* stored on its context: the reset below is an `export()`-level statement,
|
|
31
|
+
* and the comment there is where "per export, not per exporter" is argued.
|
|
32
|
+
* Moving the storage into a per-export context would make that reset
|
|
33
|
+
* implicit — the same invariant, in a place nothing says it (#2475 step 2b).
|
|
34
|
+
*/
|
|
35
|
+
ownerHistory = { fallbackRef: undefined, byEntity: new Map() };
|
|
76
36
|
/**
|
|
77
37
|
* "Can this record's line actually be read out of this store's source?"
|
|
78
38
|
* (`source-ref-bounds.ts`, #2491). Built once — `dataStore` is assigned in
|
|
@@ -95,20 +55,33 @@ export class StepExporter {
|
|
|
95
55
|
* Export to STEP format
|
|
96
56
|
*/
|
|
97
57
|
export(options) {
|
|
98
|
-
const entities = [];
|
|
99
|
-
let newEntityCount = 0;
|
|
100
58
|
// Both owner-history caches are per-EXPORT, not per-exporter: they now
|
|
101
59
|
// depend on `willBeEmitted`, which depends on this call's options. Reusing
|
|
102
60
|
// one exporter for a `visibleOnly` export and then a full one would
|
|
103
61
|
// otherwise answer the second from the first one's closure.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
62
|
+
this.ownerHistory.fallbackRef = undefined;
|
|
63
|
+
this.ownerHistory.byEntity.clear();
|
|
106
64
|
// Determine target schema from options, source schema from data store
|
|
107
65
|
const schema = options.schema || this.dataStore.schemaVersion || 'IFC4';
|
|
108
66
|
const sourceSchema = this.dataStore.schemaVersion || 'IFC4';
|
|
109
67
|
const converting = needsConversion(sourceSchema, schema);
|
|
68
|
+
// Read ONCE, here, and consumed everywhere below instead of re-spelling
|
|
69
|
+
// `options.applyMutations !== false` per site. `options` is the caller's
|
|
70
|
+
// object and this export re-enters it dozens of times; an accessor that
|
|
71
|
+
// answered differently on the second read would have let the effective
|
|
72
|
+
// index be built WITH the overlay while a later guard — including the
|
|
73
|
+
// relationship-filter precondition — decided there was none. Reading each
|
|
74
|
+
// option that feeds that precondition exactly once makes the two agree by
|
|
75
|
+
// construction rather than by every site happening to spell it the same
|
|
76
|
+
// way (adversarial review of #2668's replacement gate).
|
|
77
|
+
const applyMutations = options.applyMutations !== false;
|
|
78
|
+
// Same, for the other option the precondition reads. `isGeometryExcluded`
|
|
79
|
+
// below and both output passes' own geometry skips consume this one const,
|
|
80
|
+
// so "the gate thinks geometry is included while the predicate thinks it is
|
|
81
|
+
// excluded" is not a state this export can reach.
|
|
82
|
+
const excludeGeometry = options.includeGeometry === false;
|
|
110
83
|
if (schema === 'IFC2X3' &&
|
|
111
|
-
|
|
84
|
+
applyMutations &&
|
|
112
85
|
options.georefMutations &&
|
|
113
86
|
(Object.keys(options.georefMutations.projectedCRS ?? {}).length > 0 ||
|
|
114
87
|
Object.keys(options.georefMutations.mapConversion ?? {}).length > 0)) {
|
|
@@ -128,1151 +101,64 @@ export class StepExporter {
|
|
|
128
101
|
const schemaToken = !converting && sourceHeader?.schemaIdentifiers?.[0]
|
|
129
102
|
? sourceHeader.schemaIdentifiers[0]
|
|
130
103
|
: schema;
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// verbatim, else the generic default.
|
|
136
|
-
const description = options.description !== undefined
|
|
137
|
-
? [options.description]
|
|
138
|
-
: sourceHeader && sourceHeader.description.length > 0
|
|
139
|
-
? [...sourceHeader.description]
|
|
140
|
-
: ['Exported from ifc-lite'];
|
|
141
|
-
// Honest provenance: never claim untouched source output. Append (never
|
|
142
|
-
// overwrite) one item when ifc-lite actually changed the file.
|
|
143
|
-
if (modifications > 0) {
|
|
144
|
-
description.push(`Re-exported by ifc-lite, ${modifications} modification${modifications === 1 ? '' : 's'}`);
|
|
145
|
-
}
|
|
146
|
-
return generateHeader({
|
|
147
|
-
schema: schemaToken,
|
|
148
|
-
description,
|
|
149
|
-
implementationLevel: sourceHeader?.implementationLevel,
|
|
150
|
-
author: options.author ?? sourceHeader?.author,
|
|
151
|
-
organization: options.organization ?? sourceHeader?.organization,
|
|
152
|
-
// preprocessor_version = the tool that WROTE this file (ifc-lite);
|
|
153
|
-
// originating_system keeps the source authoring tool so it isn't erased.
|
|
154
|
-
preprocessorVersion: options.application ?? 'ifc-lite',
|
|
155
|
-
originatingSystem: sourceHeader?.originatingSystem,
|
|
156
|
-
authorization: sourceHeader?.authorization,
|
|
157
|
-
application: options.application ?? 'ifc-lite',
|
|
158
|
-
filename: options.filename ?? 'export.ifc',
|
|
159
|
-
timeStamp: options.timeStamp,
|
|
160
|
-
});
|
|
161
|
-
};
|
|
162
|
-
// The one authority for exists / class / deleted, overlay first and source
|
|
163
|
-
// buffer second. Every pass below asks this instead of `this.dataStore`,
|
|
164
|
-
// which answers only for the file as parsed (#2012).
|
|
165
|
-
const effective = getEffectiveEntityIndex(this.dataStore, this.mutationView, options.applyMutations !== false);
|
|
166
|
-
// Does this id belong to an entity the OVERLAY created (`createEntity` /
|
|
167
|
-
// `store.addEntity`) rather than to a record in the source buffer? Such an
|
|
168
|
-
// entity has no source bytes, so the source-iteration pass below never sees
|
|
169
|
-
// it and the new-entities pass at the end owns its line entirely (#2006).
|
|
170
|
-
const isOverlayCreated = (entityId) => effective.isOverlayCreated(entityId);
|
|
171
|
-
// Does this record describe a line this export can actually READ out of the
|
|
172
|
-
// source? One predicate for every byte-range gate below, so they cannot
|
|
173
|
-
// disagree — see `source-ref-bounds.ts` for the corrupt file the weaker
|
|
174
|
-
// "is there a source / does the ref claim bytes" pair let through (#2491).
|
|
175
|
-
const isReadableSourceRef = createSourceRefReader(this.dataStore.source);
|
|
176
|
-
// Build visible-only closure if requested. Classification, the closure walk
|
|
177
|
-
// and the style pass all run over the EFFECTIVE index: an overlay-created
|
|
178
|
-
// product becomes a root by the same type rules as a parsed one, the walk
|
|
179
|
-
// follows its authored references into the geometry it alone owns, and a
|
|
180
|
-
// tombstoned entity is simply not there. Run over the source buffer, a
|
|
181
|
-
// created wall could never be a root and nothing referenced it, so
|
|
182
|
-
// `visibleOnly` wrote a file without it and said nothing (#2012).
|
|
183
|
-
//
|
|
184
|
-
// Computed here, ahead of the modification-count passes below, because
|
|
185
|
-
// `hasEmittableHostBytes` needs it: a source-backed host EXCLUDED by
|
|
186
|
-
// `visibleOnly` never gets its line written by the source-iteration pass
|
|
187
|
-
// either, so counting it as "modified" would make the header claim a
|
|
188
|
-
// change the DATA section does not contain (CodeRabbit finding on #2414).
|
|
189
|
-
let allowedEntityIds = null;
|
|
190
|
-
// Populated alongside `allowedEntityIds` below. `getVisibleEntityIds`
|
|
191
|
-
// excludes a hidden PRODUCT's own line from the closure, but `IFCREL*` is
|
|
192
|
-
// an unconditional root a few lines down and its bytes are copied verbatim
|
|
193
|
-
// by the source-iteration pass — nothing there filters a `#N` the closure
|
|
194
|
-
// just excluded out of the relationship's own attribute list. Kept so the
|
|
195
|
-
// source-iteration and overlay new-entity passes can run
|
|
196
|
-
// `filterHiddenRefsFromRelationshipLine` against the SAME exclusion set
|
|
197
|
-
// `collectReferencedEntityIds` used, rather than a second, possibly
|
|
198
|
-
// divergent notion of "hidden" (#2398).
|
|
199
|
-
let hiddenProductIds = null;
|
|
200
|
-
// A relationship can name an excluded entity two ways that have nothing
|
|
201
|
-
// to do with each other: a `visibleOnly` hidden PRODUCT (`hiddenProductIds`,
|
|
202
|
-
// below), and a TOMBSTONED one — `editor.removeEntity` on a related object
|
|
203
|
-
// named by a relationship the deletion sweep below does not reach (that
|
|
204
|
-
// sweep only withholds an `IfcRelDefinesByProperties` when EVERY related
|
|
205
|
-
// object is gone, and only for that one relationship class). Left alone, a
|
|
206
|
-
// relationship still naming a deleted entity ships the identical `#N` with
|
|
207
|
-
// no `#N=` line, on a path with no `visibleOnly` involved at all (#2398).
|
|
208
|
-
// `effective.isDeleted` answers for every id, not just a precomputed set,
|
|
209
|
-
// so this predicate covers both sources without a second exclusion set.
|
|
210
|
-
//
|
|
211
|
-
// Declared here, ahead of the closure walk below, and passed into
|
|
212
|
-
// `collectReferencedEntityIds` as its `isRefExcluded` — the walk's bridge
|
|
213
|
-
// decision (whether an `IFCREL*` root may reach what it names) and the
|
|
214
|
-
// OUTPUT-line filtering further down now read the SAME predicate, rather
|
|
215
|
-
// than the walk inventing its own `!entityIndex.has` proxy for "deleted"
|
|
216
|
-
// that could disagree with this one on an id that never existed in the
|
|
217
|
-
// file at all (maintainer-found regression on #2637: such an id blocked
|
|
218
|
-
// the bridge but did not stop the relationship's own line from shipping,
|
|
219
|
-
// dropping a VISIBLE sibling's pset while adding a fresh dangling ref).
|
|
220
|
-
// A closure over the `let hiddenProductIds` above, not a value snapshot —
|
|
221
|
-
// correct because nothing reads it before `hiddenProductIds` is assigned
|
|
222
|
-
// just below.
|
|
223
|
-
const isExcludedFromRelationshipRefs = (id) => (hiddenProductIds !== null && hiddenProductIds.has(id)) || effective.isDeleted(id);
|
|
224
|
-
if (options.visibleOnly && this.dataStore.source) {
|
|
225
|
-
const visible = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null, effective);
|
|
226
|
-
hiddenProductIds = visible.hiddenProductIds;
|
|
227
|
-
allowedEntityIds = collectReferencedEntityIds(visible.roots, this.dataStore.source, effective, visible.hiddenProductIds, isExcludedFromRelationshipRefs);
|
|
228
|
-
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
229
|
-
// geometry. Styled items reference geometry items but nothing references
|
|
230
|
-
// them back, so the forward closure misses them.
|
|
231
|
-
collectStyleEntities(allowedEntityIds, this.dataStore.source, { byId: effective, byType: effective.byType });
|
|
232
|
-
}
|
|
233
|
-
// `overlayActive` proper (used everywhere else) is declared further below,
|
|
234
|
-
// ahead of the mutation-processing block it gates; duplicated here as the
|
|
235
|
-
// same expression rather than reordering that declaration.
|
|
236
|
-
const mayNameExcludedRefs = (hiddenProductIds !== null && hiddenProductIds.size > 0)
|
|
237
|
-
|| (!!this.mutationView && options.applyMutations !== false);
|
|
238
|
-
// Will THIS entity's own line ever land in the file? The same byte-range
|
|
239
|
-
// test `willBeEmitted` uses (defined further below) and the source-
|
|
240
|
-
// iteration pass's own skip at `entityRef.byteLength === 0` — a source
|
|
241
|
-
// entity with no bytes (a point-cloud / GLB "entity" from
|
|
242
|
-
// `createSyntheticDataStore`, not an overlay-created one) never gets a
|
|
243
|
-
// defining line written, source-iteration or otherwise, so a pset/attribute
|
|
244
|
-
// edit against it must not count as a modification either: the header
|
|
245
|
-
// would describe a change the file does not contain (out-of-scope finding
|
|
246
|
-
// in #2398). Also excludes a source-backed host the visible-only closure
|
|
247
|
-
// above drops — same reasoning, different reason the line never lands.
|
|
104
|
+
// The one construction site for the state this export shares across its
|
|
105
|
+
// seven phases, built in `step-pass-builder.ts` (#2475). `ExportPass` in
|
|
106
|
+
// `step-export-types.ts` says what belongs on it and what deliberately
|
|
107
|
+
// does not.
|
|
248
108
|
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// `
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Collect entities that need to be modified or created
|
|
292
|
-
const modifiedEntities = new Set();
|
|
293
|
-
const modifiedAttributes = new Map();
|
|
294
|
-
const newPropertySets = [];
|
|
295
|
-
const newQuantitySets = [];
|
|
296
|
-
const typeOwnedPsetNamesByEntity = new Map();
|
|
297
|
-
const typeOwnedPsetIdsByEntity = new Map();
|
|
298
|
-
const rewrittenEntityIds = new Set();
|
|
299
|
-
const rewrittenEntityLines = new Map();
|
|
300
|
-
/** HasPropertySets slot value for an OVERLAY-CREATED type object, applied
|
|
301
|
-
* by the new-entities pass (there is no source line to rewrite). */
|
|
302
|
-
const overlayTypeOwnedPsets = new Map();
|
|
303
|
-
// Track property set IDs and relationship IDs to skip
|
|
304
|
-
const skipPropertySetIds = new Set();
|
|
305
|
-
const skipRelationshipIds = new Set();
|
|
306
|
-
const overlayActive = !!this.mutationView && (options.applyMutations !== false);
|
|
307
|
-
// Process mutations if we have a mutation view
|
|
308
|
-
if (this.mutationView && (options.applyMutations !== false)) {
|
|
309
|
-
const mutations = this.mutationView.getMutations();
|
|
310
|
-
// Attribute values come from the *overlay*, never from the mutation
|
|
311
|
-
// history. The history is append-only and undo writes its reverse edit
|
|
312
|
-
// with `skipHistory: true`, so a superseded UPDATE_ATTRIBUTE record keeps
|
|
313
|
-
// its stale `newValue` forever — replaying it resurrects edits the user
|
|
314
|
-
// undid (#1957). The overlay is what the editor shows, and it is already
|
|
315
|
-
// the source for psets, quantities, positional attributes and retypes
|
|
316
|
-
// below, so attributes were the sole outlier.
|
|
317
|
-
for (const [entityId, attrs] of this.mutationView.getAttributeMutationsByEntity()) {
|
|
318
|
-
modifiedEntities.add(entityId);
|
|
319
|
-
let target = modifiedAttributes.get(entityId);
|
|
320
|
-
if (!target) {
|
|
321
|
-
target = new Map();
|
|
322
|
-
modifiedAttributes.set(entityId, target);
|
|
323
|
-
}
|
|
324
|
-
for (const [name, value] of attrs)
|
|
325
|
-
target.set(name, value);
|
|
326
|
-
}
|
|
327
|
-
// Group mutations by entity, separating property vs quantity mutations
|
|
328
|
-
const entityPropMutations = new Map();
|
|
329
|
-
const entityQuantMutations = new Map();
|
|
330
|
-
for (const mutation of mutations) {
|
|
331
|
-
// Handled above, off the overlay. Skipped explicitly because an
|
|
332
|
-
// UPDATE_ATTRIBUTE record can also carry a `psetName` (georef fields
|
|
333
|
-
// encode their target entity there) and must not be mistaken for a
|
|
334
|
-
// property-set edit.
|
|
335
|
-
if (mutation.type === 'UPDATE_ATTRIBUTE')
|
|
336
|
-
continue;
|
|
337
|
-
if (!mutation.psetName)
|
|
338
|
-
continue;
|
|
339
|
-
const isQuantity = mutation.type === 'CREATE_QUANTITY' || mutation.type === 'UPDATE_QUANTITY'
|
|
340
|
-
|| mutation.type === 'DELETE_QUANTITY' || mutation.type === 'DELETE_QUANTITY_SET';
|
|
341
|
-
const targetMap = isQuantity ? entityQuantMutations : entityPropMutations;
|
|
342
|
-
if (!targetMap.has(mutation.entityId)) {
|
|
343
|
-
targetMap.set(mutation.entityId, new Set());
|
|
344
|
-
}
|
|
345
|
-
targetMap.get(mutation.entityId).add(mutation.psetName);
|
|
346
|
-
}
|
|
347
|
-
// Build a reverse index of IfcRelDefinesByProperties → (relId, psetId)
|
|
348
|
-
// pairs keyed on each related entity. The two property/quantity loops
|
|
349
|
-
// below previously walked every entity in `entityIndex.byId` per
|
|
350
|
-
// modified entity (O(E·N)); the index keeps the per-entity step
|
|
351
|
-
// O(K) where K is the number of rels referencing that entity.
|
|
352
|
-
const { byEntity: relDefinesByEntity, relatedByRel } = this.buildRelDefinesByPropertiesIndex();
|
|
353
|
-
// A source IfcRelDefinesByProperties whose EVERY related object the
|
|
354
|
-
// session deleted has nothing left to relate, and emitting it leaves a
|
|
355
|
-
// `#id` pointing at a record the export skipped. Dropped only when all of
|
|
356
|
-
// them are gone: a rel that still names a live entity is that entity's
|
|
357
|
-
// only link to its psets, and nothing here rewrites a RelatedObjects list.
|
|
358
|
-
for (const [relId, related] of relatedByRel) {
|
|
359
|
-
if (related.length > 0 && related.every((id) => effective.isDeleted(id))) {
|
|
360
|
-
skipRelationshipIds.add(relId);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Collect modified property sets and find original psets to skip
|
|
364
|
-
for (const [entityId, psetNames] of entityPropMutations) {
|
|
365
|
-
// A deleted entity must not cause the exporter to REMOVE anything.
|
|
366
|
-
//
|
|
367
|
-
// This is the other half of the dangling-reference class, and the half
|
|
368
|
-
// `willBeEmitted` cannot reach: that predicate guards what gets ADDED,
|
|
369
|
-
// and this loop's real work is deciding what gets SKIPPED. An edited
|
|
370
|
-
// pset is replaced wholesale, so its original id goes into
|
|
371
|
-
// `skipPropertySetIds` — but IFC exporters share one IfcPropertySet
|
|
372
|
-
// between entities, and once the host is deleted there is no
|
|
373
|
-
// replacement to take its place. The surviving entity's relation then
|
|
374
|
-
// points at a container nobody wrote. Verified against main at
|
|
375
|
-
// e6516991 (#2030's own merge): edit `Pset_WallCommon` on one of two
|
|
376
|
-
// walls sharing it, delete that wall, and the export drops #11 while
|
|
377
|
-
// #12 still names it. `retainSharedAtoms` rescues a shared ATOM one
|
|
378
|
-
// level down; nothing rescues the shared container.
|
|
379
|
-
//
|
|
380
|
-
// Leaving the pset alone makes it an orphan when nothing else
|
|
381
|
-
// references it, which is valid IFC. Its relation is dropped by the
|
|
382
|
-
// sweep above, which handles a plain delete too — no pset edit needed.
|
|
383
|
-
if (effective.isDeleted(entityId))
|
|
384
|
-
continue;
|
|
385
|
-
modifiedEntities.add(entityId);
|
|
386
|
-
// Same rule as the attribute loop below: an overlay-CREATED entity is
|
|
387
|
-
// emitted once, by the new-entities pass, and already counted in
|
|
388
|
-
// `newEntityCount` — as are the pset entities this loop goes on to
|
|
389
|
-
// generate. Only the COUNT is guarded; the entity still records its
|
|
390
|
-
// pset edits and still emits them.
|
|
391
|
-
//
|
|
392
|
-
// A NOMINATION, in both modes, never a count on its own: this site sees
|
|
393
|
-
// a pset NAME the session touched, not whether that name resolves to
|
|
394
|
-
// anything. `deletePropertySet(id, 'AName')` on a host that owns no such
|
|
395
|
-
// set reaches here and changes nothing at all, and used to put "1
|
|
396
|
-
// modification" in the header of a byte-identical file (#2474). What
|
|
397
|
-
// settles it is the generator's `recordEmitted` and the skip branches'
|
|
398
|
-
// `recordWithheld` below.
|
|
399
|
-
if (!isOverlayCreated(entityId) && hasEmittableHostBytes(entityId)) {
|
|
400
|
-
modifications.nominate(entityId, 'property-set');
|
|
401
|
-
}
|
|
402
|
-
// Get the FULL mutated property sets for this entity (merged base + mutations)
|
|
403
|
-
const allPsets = this.mutationView.getForEntity(entityId);
|
|
404
|
-
const relevantPsets = allPsets.filter((pset) => psetNames.has(pset.name));
|
|
405
|
-
const relDefinedPsetNames = new Set();
|
|
406
|
-
if (relevantPsets.length > 0) {
|
|
407
|
-
newPropertySets.push({ entityId, psets: relevantPsets });
|
|
408
|
-
}
|
|
409
|
-
// Find original property set IDs and relationship IDs to skip — look
|
|
410
|
-
// up only the IfcRelDefinesByProperties rels that reference this entity.
|
|
411
|
-
const rels = relDefinesByEntity.get(entityId);
|
|
412
|
-
if (rels) {
|
|
413
|
-
for (const { relId, psetId: relatedPsetId } of rels) {
|
|
414
|
-
// Check if this pset is one we're modifying
|
|
415
|
-
const psetName = this.getPropertySetName(relatedPsetId);
|
|
416
|
-
if (psetName) {
|
|
417
|
-
relDefinedPsetNames.add(psetName);
|
|
418
|
-
}
|
|
419
|
-
if (psetName && psetNames.has(psetName)) {
|
|
420
|
-
skipRelationshipIds.add(relId);
|
|
421
|
-
skipPropertySetIds.add(relatedPsetId);
|
|
422
|
-
// Also skip the individual properties in this pset
|
|
423
|
-
const propIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
424
|
-
for (const propId of propIds) {
|
|
425
|
-
skipPropertySetIds.add(propId);
|
|
426
|
-
}
|
|
427
|
-
// The other half of "did this edit change the file": a full export
|
|
428
|
-
// applies a set DELETION by leaving these lines out, and produces
|
|
429
|
-
// no replacement content to record an emission for. Without this
|
|
430
|
-
// the count would settle from the generator alone and a real
|
|
431
|
-
// deletion would stop counting along with the no-op one (#2474).
|
|
432
|
-
modifications.recordWithheld(entityId, 'property-set');
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
if (isTypeClass(effective.typeOf(entityId))) {
|
|
437
|
-
const typeOwnedPsetIds = this.getTypeOwnedHasPropertySetIds(entityId, effective);
|
|
438
|
-
const typeOwnedAffected = new Set();
|
|
439
|
-
for (const psetId of typeOwnedPsetIds) {
|
|
440
|
-
const psetName = this.getPropertySetName(psetId);
|
|
441
|
-
if (!psetName || !psetNames.has(psetName))
|
|
442
|
-
continue;
|
|
443
|
-
typeOwnedAffected.add(psetName);
|
|
444
|
-
skipPropertySetIds.add(psetId);
|
|
445
|
-
const propIds = this.getPropertyIdsInSet(psetId);
|
|
446
|
-
for (const propId of propIds) {
|
|
447
|
-
skipPropertySetIds.add(propId);
|
|
448
|
-
}
|
|
449
|
-
// No `recordWithheld` twin of the rel-defined branch above, and
|
|
450
|
-
// deliberately: a name that matches an OWNED pset is either dropped
|
|
451
|
-
// from the resolved list or swapped for the replacement this export
|
|
452
|
-
// generated, so slot 5 always comes back different and the repoint
|
|
453
|
-
// below records the emission for it. A second record here would be
|
|
454
|
-
// one no mutation can kill.
|
|
455
|
-
}
|
|
456
|
-
for (const psetName of psetNames) {
|
|
457
|
-
if (!relDefinedPsetNames.has(psetName)) {
|
|
458
|
-
typeOwnedAffected.add(psetName);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (typeOwnedAffected.size > 0) {
|
|
462
|
-
typeOwnedPsetNamesByEntity.set(entityId, typeOwnedAffected);
|
|
463
|
-
typeOwnedPsetIdsByEntity.set(entityId, typeOwnedPsetIds);
|
|
464
|
-
rewrittenEntityIds.add(entityId);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
// Collect modified quantity sets (only if quantities are included)
|
|
469
|
-
if (options.includeQuantities === false)
|
|
470
|
-
entityQuantMutations.clear();
|
|
471
|
-
// A quantity overlay with nothing under it regenerates a source quantity
|
|
472
|
-
// set from the edited quantity ALONE, and the skip loop below then
|
|
473
|
-
// withholds the source lines that held its siblings (#2487). Unlike
|
|
474
|
-
// properties — whose base falls back to the `baseTable` the view was
|
|
475
|
-
// constructed with — quantities have only the opt-in
|
|
476
|
-
// `setQuantityExtractor`, so the default really is an empty base, and
|
|
477
|
-
// four in-tree callers plus every external embedder never set it.
|
|
478
|
-
//
|
|
479
|
-
// The exporter is the one place that always holds the missing half: it
|
|
480
|
-
// was handed the very store the view is an overlay ON. Supplying it here
|
|
481
|
-
// makes the loss impossible for every caller rather than for the callers
|
|
482
|
-
// we happened to find, and a view that resolves its own quantities (the
|
|
483
|
-
// viewer, MCP, the CLI headless backend) is never overwritten.
|
|
484
|
-
//
|
|
485
|
-
// The extractor closes over ONE store, and the view outlives this export.
|
|
486
|
-
// So it closes over a BOX this class owns instead: a second export of the
|
|
487
|
-
// same view against a DIFFERENT store re-points that box rather than
|
|
488
|
-
// reading the first store's quantities, which is the one way "install only
|
|
489
|
-
// when absent" could have answered from the wrong file. The setter is
|
|
490
|
-
// called at most once per view, so a caller that installs its own
|
|
491
|
-
// extractor at any point — before the first export or after it — keeps it.
|
|
492
|
-
//
|
|
493
|
-
// `hasQuantityBase` and `setQuantityExtractor` are probed, like every other
|
|
494
|
-
// optional view capability this class reaches for (`peekNextExpressId`,
|
|
495
|
-
// `getNewEntities`, `getEntityTypeMutation`): `MutablePropertyView` is
|
|
496
|
-
// published API arriving from a separately versioned package, and callers
|
|
497
|
-
// pass partial and duck-typed views. `hasQuantityBase` is newer than
|
|
498
|
-
// `setQuantityExtractor`, and without it there is no way to tell an empty
|
|
499
|
-
// base from a caller-supplied one — so an older view falls back to the
|
|
500
|
-
// pre-#2487 behaviour (no base supplied) rather than risk overwriting one.
|
|
501
|
-
const quantityView = this.mutationView;
|
|
502
|
-
if (entityQuantMutations.size > 0 &&
|
|
503
|
-
typeof quantityView.setQuantityExtractor === 'function' &&
|
|
504
|
-
typeof quantityView.hasQuantityBase === 'function') {
|
|
505
|
-
const installed = exporterQuantityBase.get(quantityView);
|
|
506
|
-
if (installed) {
|
|
507
|
-
// Ours, or a caller's that replaced ours: re-pointing the box is a
|
|
508
|
-
// no-op in the second case, and calling the setter again is what
|
|
509
|
-
// would not be.
|
|
510
|
-
installed.store = this.dataStore;
|
|
511
|
-
}
|
|
512
|
-
else if (!quantityView.hasQuantityBase()) {
|
|
513
|
-
const box = { store: this.dataStore };
|
|
514
|
-
exporterQuantityBase.set(quantityView, box);
|
|
515
|
-
quantityView.setQuantityExtractor((id) => extractQuantitiesOnDemand(box.store, id));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
for (const [entityId, qsetNames] of entityQuantMutations) {
|
|
519
|
-
// Same rule as the property loop above: a deleted entity removes nothing.
|
|
520
|
-
if (effective.isDeleted(entityId))
|
|
521
|
-
continue;
|
|
522
|
-
modifiedEntities.add(entityId);
|
|
523
|
-
// See the property loop above — an overlay-created entity is counted as
|
|
524
|
-
// new, not modified. The pset loop's own nomination no longer has to be
|
|
525
|
-
// excluded to avoid a double count: the ledger settles per ENTITY, so a
|
|
526
|
-
// host with both a pset and a qset edit counts once whatever is
|
|
527
|
-
// nominated. Nominating both buys the opposite — an accurate warning
|
|
528
|
-
// when the qset half is the half a delta cannot carry.
|
|
529
|
-
//
|
|
530
|
-
// Settled from effect like its property-set twin (#2474). The reachable
|
|
531
|
-
// no-op here is an UNDONE quantity-set creation whose name matches NO
|
|
532
|
-
// source set: `getMutations()` is append-only, so the `CREATE_QUANTITY`
|
|
533
|
-
// record still names the qset after `removeQuantityMutation` has taken
|
|
534
|
-
// it out of the overlay, and the generator below then finds nothing to
|
|
535
|
-
// write. The same undo against a COLLIDING name is not a no-op — it
|
|
536
|
-
// withholds the source set's lines — which is what the skip loop's
|
|
537
|
-
// `recordWithheld` below settles.
|
|
538
|
-
if (!isOverlayCreated(entityId) && hasEmittableHostBytes(entityId)) {
|
|
539
|
-
modifications.nominate(entityId, 'quantity-set');
|
|
540
|
-
}
|
|
541
|
-
const allQsets = this.mutationView.getQuantitiesForEntity(entityId);
|
|
542
|
-
const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
|
|
543
|
-
if (relevantQsets.length > 0) {
|
|
544
|
-
newQuantitySets.push({ entityId, qsets: relevantQsets });
|
|
545
|
-
}
|
|
546
|
-
// The names this export is actually WRITING a replacement for. The
|
|
547
|
-
// affected-name set is not the same thing: it comes from the session's
|
|
548
|
-
// append-only mutation history, which keeps naming a quantity set after
|
|
549
|
-
// an undo has taken it back out of the overlay, so a Ctrl+Z used to
|
|
550
|
-
// withhold a source `IfcElementQuantity` that nothing regenerated.
|
|
551
|
-
//
|
|
552
|
-
// A quantity-set REMOVAL is the one case where withholding WITHOUT a
|
|
553
|
-
// replacement is the intent rather than the bug. It had no public
|
|
554
|
-
// populator when #2487 wrote that rule, so the rule read "always the
|
|
555
|
-
// bug"; `MutablePropertyView.deleteQuantitySet` (#2508) gives it one,
|
|
556
|
-
// and the deleted set is now asked for by name below. Without that, the
|
|
557
|
-
// panel hid a base quantity set the exported file still carried.
|
|
558
|
-
const regeneratedQsetNames = new Set(relevantQsets.map((qset) => qset.name));
|
|
559
|
-
// Skip original quantity set entities (IfcElementQuantity).
|
|
560
|
-
// Same per-entity index lookup as the property branch above.
|
|
561
|
-
const rels = relDefinesByEntity.get(entityId);
|
|
562
|
-
if (rels) {
|
|
563
|
-
for (const { relId, psetId: relatedPsetId } of rels) {
|
|
564
|
-
const qsetName = this.getElementQuantityName(relatedPsetId);
|
|
565
|
-
const deleted = qsetName !== null
|
|
566
|
-
&& this.mutationView.isQuantitySetDeleted?.(entityId, qsetName) === true;
|
|
567
|
-
if (qsetName && (regeneratedQsetNames.has(qsetName) || deleted)) {
|
|
568
|
-
skipRelationshipIds.add(relId);
|
|
569
|
-
skipPropertySetIds.add(relatedPsetId);
|
|
570
|
-
const quantIds = this.getPropertyIdsInSet(relatedPsetId);
|
|
571
|
-
for (const quantId of quantIds) {
|
|
572
|
-
skipPropertySetIds.add(quantId);
|
|
573
|
-
}
|
|
574
|
-
// The withheld half, exactly as the rel-defined property branch
|
|
575
|
-
// above. This loop has just decided that #`relatedPsetId`, its
|
|
576
|
-
// quantity atoms and the relationship that attached them do NOT
|
|
577
|
-
// go into the file; whether anything is generated to take their
|
|
578
|
-
// place is decided elsewhere, and is not this branch's to assume.
|
|
579
|
-
//
|
|
580
|
-
// It IS assumable for the pset side and not here, and the
|
|
581
|
-
// difference is where the two read their base from.
|
|
582
|
-
// `getForEntity` merges the overlay over the base pset walk, so a
|
|
583
|
-
// name the session touched but did not change still resolves to
|
|
584
|
-
// source content and is regenerated.
|
|
585
|
-
// `getQuantitiesForEntity` merges the overlay over
|
|
586
|
-
// `quantityExtractor`, which is OPT-IN: it defaults to null, and
|
|
587
|
-
// several in-tree callers wire the property extractor beside it
|
|
588
|
-
// and not it (`cli/commands/mutate.ts`, `gym.ts`,
|
|
589
|
-
// `generate-spaces.ts`, `export/demesh-session.ts`), as does any
|
|
590
|
-
// external embedder of these two published packages. With no
|
|
591
|
-
// extractor the base is empty and the overlay is the only source,
|
|
592
|
-
// so a qset the overlay no longer holds resolves to nothing.
|
|
593
|
-
//
|
|
594
|
-
// Which makes this reachable through an UNDONE quantity-set
|
|
595
|
-
// creation whose name COLLIDES with a source set:
|
|
596
|
-
// `setQuantity(id, 'Qto_WallBaseQuantities', ...)` followed by the
|
|
597
|
-
// `removeQuantityMutation` that mutationSlice runs on Ctrl+Z. The
|
|
598
|
-
// append-only history still names the qset, so this branch
|
|
599
|
-
// withholds the source lines; the overlay is empty again, so
|
|
600
|
-
// nothing is regenerated. The export drops the source quantity set
|
|
601
|
-
// — a real change to the file, and a data-loss bug of its own
|
|
602
|
-
// (#2487) — and this call is what stops the count from calling it
|
|
603
|
-
// nothing.
|
|
604
|
-
modifications.recordWithheld(entityId, 'quantity-set');
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
for (const [entityId] of modifiedAttributes) {
|
|
610
|
-
// An overlay-CREATED entity carrying attribute edits is emitted once,
|
|
611
|
-
// by the new-entities pass, and already counted in `newEntityCount`.
|
|
612
|
-
// Counting it here too made the header claim two affected entities for
|
|
613
|
-
// one created-then-renamed wall.
|
|
614
|
-
if (isOverlayCreated(entityId))
|
|
615
|
-
continue;
|
|
616
|
-
// A source entity with no bytes never gets its line rewritten (the
|
|
617
|
-
// source-iteration pass skips it), so an attribute edit against it
|
|
618
|
-
// must not inflate the count either.
|
|
619
|
-
if (!hasEmittableHostBytes(entityId))
|
|
620
|
-
continue;
|
|
621
|
-
// Under `deltaOnly` this only NOMINATES the host's ATTRIBUTE edits:
|
|
622
|
-
// nothing writes an in-place attribute edit into a delta except the
|
|
623
|
-
// type-object line rewrite, so the ledger drops it at settle time
|
|
624
|
-
// unless that pass reports having carried it (#2462). That nomination
|
|
625
|
-
// is deliberately made at INTENT: the per-kind warning exists to NAME
|
|
626
|
-
// an edit the delta could not carry, and an undeliverable edit is
|
|
627
|
-
// exactly the one that must still be named.
|
|
628
|
-
//
|
|
629
|
-
// A FULL export has no such warning, so an edit that resolved to
|
|
630
|
-
// nothing has nothing to say and nothing to claim — it waits for the
|
|
631
|
-
// rewrite instead. `setAttribute` to the value already in the slot, and
|
|
632
|
-
// `setAttribute` naming a slot the class does not declare, both leave
|
|
633
|
-
// the line byte-identical and used to count anyway (#2483).
|
|
634
|
-
//
|
|
635
|
-
// Recorded unconditionally. It used to be skipped for a host that also
|
|
636
|
-
// had a pset or qset edit, because the count was per entity and the
|
|
637
|
-
// other loop had already nominated it — which is exactly what let a
|
|
638
|
-
// pset emission mark the rename delivered and suppress its warning. The
|
|
639
|
-
// ledger de-duplicates the COUNT per entity now, so the two edits can
|
|
640
|
-
// and must be nominated separately.
|
|
641
|
-
inPlaceNominees.attribute.add(entityId);
|
|
642
|
-
if (options.deltaOnly === true)
|
|
643
|
-
modifications.nominate(entityId, 'attribute');
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// Process georeferencing mutations (only when applyMutations is enabled)
|
|
647
|
-
const newGeorefLines = [];
|
|
648
|
-
const warnings = [];
|
|
649
|
-
if (options.applyMutations !== false && options.georefMutations) {
|
|
650
|
-
const gm = options.georefMutations;
|
|
651
|
-
// `effective.byType`, not the raw index: a source IfcProjectedCRS the
|
|
652
|
-
// session tombstoned is still in `dataStore.entityIndex`, so the modify
|
|
653
|
-
// branch below would queue attribute edits against an id the
|
|
654
|
-
// source-iteration pass then skips — the replacement georeferencing
|
|
655
|
-
// vanishes from the file with no error. `effective.byType` drops
|
|
656
|
-
// tombstones and adds overlay-created records, which the new-entities
|
|
657
|
-
// pass applies `modifiedAttributes` to, so both branches agree on which
|
|
658
|
-
// georeferencing entities exist (#2048).
|
|
659
|
-
const existingCrsIds = effective.byType.get('IFCPROJECTEDCRS');
|
|
660
|
-
const existingMcIds = effective.byType.get('IFCMAPCONVERSION');
|
|
661
|
-
// Modify existing IfcProjectedCRS
|
|
662
|
-
if (gm.projectedCRS && existingCrsIds?.length) {
|
|
663
|
-
const entityId = existingCrsIds[0];
|
|
664
|
-
if (!modifiedAttributes.has(entityId)) {
|
|
665
|
-
modifiedAttributes.set(entityId, new Map());
|
|
666
|
-
}
|
|
667
|
-
const attrMap = modifiedAttributes.get(entityId);
|
|
668
|
-
const crs = gm.projectedCRS;
|
|
669
|
-
let changed = false;
|
|
670
|
-
if (crs.name !== undefined) {
|
|
671
|
-
attrMap.set('Name', String(crs.name));
|
|
672
|
-
changed = true;
|
|
673
|
-
}
|
|
674
|
-
if (crs.description !== undefined) {
|
|
675
|
-
attrMap.set('Description', String(crs.description));
|
|
676
|
-
changed = true;
|
|
677
|
-
}
|
|
678
|
-
if (crs.geodeticDatum !== undefined) {
|
|
679
|
-
attrMap.set('GeodeticDatum', String(crs.geodeticDatum));
|
|
680
|
-
changed = true;
|
|
681
|
-
}
|
|
682
|
-
if (crs.verticalDatum !== undefined) {
|
|
683
|
-
attrMap.set('VerticalDatum', String(crs.verticalDatum));
|
|
684
|
-
changed = true;
|
|
685
|
-
}
|
|
686
|
-
if (crs.mapProjection !== undefined) {
|
|
687
|
-
attrMap.set('MapProjection', String(crs.mapProjection));
|
|
688
|
-
changed = true;
|
|
689
|
-
}
|
|
690
|
-
if (crs.mapZone !== undefined) {
|
|
691
|
-
attrMap.set('MapZone', String(crs.mapZone));
|
|
692
|
-
changed = true;
|
|
693
|
-
}
|
|
694
|
-
if (crs.mapUnit !== undefined) {
|
|
695
|
-
const mapUnitRef = this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines, effective);
|
|
696
|
-
attrMap.set('MapUnit', `#${mapUnitRef}`);
|
|
697
|
-
changed = true;
|
|
698
|
-
}
|
|
699
|
-
if (changed) {
|
|
700
|
-
modifiedEntities.add(entityId);
|
|
701
|
-
// Queued as attribute edits, which only the source-iteration pass
|
|
702
|
-
// writes — so under `deltaOnly` this nominates and settle decides.
|
|
703
|
-
// Recorded even when the host is already in `modifiedEntities`: that
|
|
704
|
-
// guard existed to stop a second COUNT, which the ledger now handles
|
|
705
|
-
// per entity, and suppressing the nomination would hide a dropped
|
|
706
|
-
// georeferencing edit behind an unrelated edit to the same record.
|
|
707
|
-
//
|
|
708
|
-
// `changed` above is INTENT — a field was supplied, not a field that
|
|
709
|
-
// differs from the one in the file. Writing `name: 'EPSG:2056'` over
|
|
710
|
-
// an IfcProjectedCRS already named `EPSG:2056` leaves the line
|
|
711
|
-
// byte-identical, so a full export waits for the rewrite exactly as
|
|
712
|
-
// the plain attribute site does (#2483).
|
|
713
|
-
if (hasEmittableHostBytes(entityId)) {
|
|
714
|
-
inPlaceNominees.georeferencing.add(entityId);
|
|
715
|
-
if (options.deltaOnly === true)
|
|
716
|
-
modifications.nominate(entityId, 'georeferencing');
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
// Modify existing IfcMapConversion
|
|
721
|
-
if (gm.mapConversion && existingMcIds?.length) {
|
|
722
|
-
const entityId = existingMcIds[0];
|
|
723
|
-
if (!modifiedAttributes.has(entityId)) {
|
|
724
|
-
modifiedAttributes.set(entityId, new Map());
|
|
725
|
-
}
|
|
726
|
-
const attrMap = modifiedAttributes.get(entityId);
|
|
727
|
-
const mc = gm.mapConversion;
|
|
728
|
-
let changed = false;
|
|
729
|
-
if (mc.eastings !== undefined) {
|
|
730
|
-
attrMap.set('Eastings', String(mc.eastings));
|
|
731
|
-
changed = true;
|
|
732
|
-
}
|
|
733
|
-
if (mc.northings !== undefined) {
|
|
734
|
-
attrMap.set('Northings', String(mc.northings));
|
|
735
|
-
changed = true;
|
|
736
|
-
}
|
|
737
|
-
if (mc.orthogonalHeight !== undefined) {
|
|
738
|
-
attrMap.set('OrthogonalHeight', String(mc.orthogonalHeight));
|
|
739
|
-
changed = true;
|
|
740
|
-
}
|
|
741
|
-
if (mc.xAxisAbscissa !== undefined) {
|
|
742
|
-
attrMap.set('XAxisAbscissa', String(mc.xAxisAbscissa));
|
|
743
|
-
changed = true;
|
|
744
|
-
}
|
|
745
|
-
if (mc.xAxisOrdinate !== undefined) {
|
|
746
|
-
attrMap.set('XAxisOrdinate', String(mc.xAxisOrdinate));
|
|
747
|
-
changed = true;
|
|
748
|
-
}
|
|
749
|
-
if (mc.scale !== undefined) {
|
|
750
|
-
attrMap.set('Scale', String(mc.scale));
|
|
751
|
-
changed = true;
|
|
752
|
-
}
|
|
753
|
-
if (changed) {
|
|
754
|
-
modifiedEntities.add(entityId);
|
|
755
|
-
// Same as the IfcProjectedCRS branch above, effect gate included.
|
|
756
|
-
if (hasEmittableHostBytes(entityId)) {
|
|
757
|
-
inPlaceNominees.georeferencing.add(entityId);
|
|
758
|
-
if (options.deltaOnly === true)
|
|
759
|
-
modifications.nominate(entityId, 'georeferencing');
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
// CREATE new georef entities when file has none
|
|
764
|
-
if (gm.projectedCRS && !existingCrsIds?.length) {
|
|
765
|
-
const crs = gm.projectedCRS;
|
|
766
|
-
const crsId = this.nextExpressId++;
|
|
767
|
-
// IfcProjectedCRS(Name, Description, GeodeticDatum, VerticalDatum, MapProjection, MapZone, MapUnit)
|
|
768
|
-
const name = crs.name ? `'${escapeStepString(String(crs.name))}'` : '$';
|
|
769
|
-
const desc = crs.description ? `'${escapeStepString(String(crs.description))}'` : '$';
|
|
770
|
-
const datum = crs.geodeticDatum ? `'${escapeStepString(String(crs.geodeticDatum))}'` : '$';
|
|
771
|
-
const vDatum = crs.verticalDatum ? `'${escapeStepString(String(crs.verticalDatum))}'` : '$';
|
|
772
|
-
const proj = crs.mapProjection ? `'${escapeStepString(String(crs.mapProjection))}'` : '$';
|
|
773
|
-
const zone = crs.mapZone ? `'${escapeStepString(String(crs.mapZone))}'` : '$';
|
|
774
|
-
const mapUnitRef = crs.mapUnit
|
|
775
|
-
? `#${this.resolveMapUnitReference(String(crs.mapUnit), newGeorefLines, effective)}`
|
|
776
|
-
: '$';
|
|
777
|
-
newGeorefLines.push(`#${crsId}=IFCPROJECTEDCRS(${name},${desc},${datum},${vDatum},${proj},${zone},${mapUnitRef});`);
|
|
778
|
-
newEntityCount++;
|
|
779
|
-
// Find IfcGeometricRepresentationContext as SourceCRS for MapConversion
|
|
780
|
-
const contextId = this.findPreferredGeometricRepresentationContextId(effective);
|
|
781
|
-
if (contextId) {
|
|
782
|
-
const mc = gm.mapConversion || {};
|
|
783
|
-
const mcId = this.nextExpressId++;
|
|
784
|
-
const eastings = toStepReal(Number(mc.eastings) || 0);
|
|
785
|
-
const northings = toStepReal(Number(mc.northings) || 0);
|
|
786
|
-
const height = toStepReal(Number(mc.orthogonalHeight) || 0);
|
|
787
|
-
const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
|
|
788
|
-
const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
|
|
789
|
-
const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
|
|
790
|
-
// IfcMapConversion(SourceCRS, TargetCRS, Eastings, Northings, OrthogonalHeight, XAxisAbscissa, XAxisOrdinate, Scale)
|
|
791
|
-
newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${crsId},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
|
|
792
|
-
newEntityCount++;
|
|
793
|
-
}
|
|
794
|
-
else {
|
|
795
|
-
this.reportMapConversionRefused(warnings);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
else if (gm.mapConversion && !existingMcIds?.length && existingCrsIds?.length) {
|
|
799
|
-
// CRS exists but no MapConversion — create just the conversion
|
|
800
|
-
const contextId = this.findPreferredGeometricRepresentationContextId(effective);
|
|
801
|
-
if (contextId) {
|
|
802
|
-
const mc = gm.mapConversion;
|
|
803
|
-
const mcId = this.nextExpressId++;
|
|
804
|
-
const eastings = toStepReal(Number(mc.eastings) || 0);
|
|
805
|
-
const northings = toStepReal(Number(mc.northings) || 0);
|
|
806
|
-
const height = toStepReal(Number(mc.orthogonalHeight) || 0);
|
|
807
|
-
const abscissa = mc.xAxisAbscissa !== undefined ? toStepReal(Number(mc.xAxisAbscissa)) : '$';
|
|
808
|
-
const ordinate = mc.xAxisOrdinate !== undefined ? toStepReal(Number(mc.xAxisOrdinate)) : '$';
|
|
809
|
-
const scale = mc.scale !== undefined ? toStepReal(Number(mc.scale)) : '$';
|
|
810
|
-
newGeorefLines.push(`#${mcId}=IFCMAPCONVERSION(#${contextId},#${existingCrsIds[0]},${eastings},${northings},${height},${abscissa},${ordinate},${scale});`);
|
|
811
|
-
newEntityCount++;
|
|
812
|
-
}
|
|
813
|
-
else {
|
|
814
|
-
this.reportMapConversionRefused(warnings);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
else if (gm.mapConversion && !existingMcIds?.length && !existingCrsIds?.length) {
|
|
818
|
-
// A map conversion was requested, but there is no IfcProjectedCRS to
|
|
819
|
-
// reference as TargetCRS: none was requested (the first branch above
|
|
820
|
-
// didn't fire) and none exists in the file. Both CREATE branches are
|
|
821
|
-
// skipped, so nothing is attempted — report the refusal so the
|
|
822
|
-
// caller isn't left with an empty stats.warnings and no hint (#2105).
|
|
823
|
-
this.reportMapConversionRefusedNoCrs(warnings);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
// If delta only, only export modified entities. Overlay-created entities
|
|
827
|
-
// also count — without this, `createEntity()`-only edits would silently
|
|
828
|
-
// drop out of delta exports.
|
|
829
|
-
const overlayNewEntityCount = (this.mutationView
|
|
830
|
-
&& options.applyMutations !== false
|
|
831
|
-
&& typeof this.mutationView.getNewEntities === 'function') ? this.mutationView.getNewEntities().length : 0;
|
|
832
|
-
// Georef-only deltas (newGeorefLines populated but no entity changes) must
|
|
833
|
-
// still produce a non-empty DATA section.
|
|
834
|
-
if (options.deltaOnly
|
|
835
|
-
&& modifiedEntities.size === 0
|
|
836
|
-
&& overlayNewEntityCount === 0
|
|
837
|
-
&& newGeorefLines.length === 0) {
|
|
838
|
-
const emptyContent = new TextEncoder().encode(buildHeader(0) + 'DATA;\nENDSEC;\nEND-ISO-10303-21;\n');
|
|
839
|
-
return {
|
|
840
|
-
content: emptyContent,
|
|
841
|
-
stats: {
|
|
842
|
-
entityCount: 0,
|
|
843
|
-
newEntityCount: 0,
|
|
844
|
-
modifiedEntityCount: 0,
|
|
845
|
-
fileSize: emptyContent.byteLength,
|
|
846
|
-
warnings,
|
|
847
|
-
},
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Will this id have a defining STEP line in the output at all?
|
|
852
|
-
*
|
|
853
|
-
* The predicate is #2030's, and it is the right one: the pset, quantity and
|
|
854
|
-
* type-owned passes below are built from unfiltered mutation history, and
|
|
855
|
-
* what each of them needs to know before emitting an
|
|
856
|
-
* `IFCRELDEFINESBYPROPERTIES` is not "was this deleted" or "is this hidden"
|
|
857
|
-
* but the general question those are two answers to. A relation naming an
|
|
858
|
-
* expressId that never gets written is a dangling reference and an invalid
|
|
859
|
-
* file, whichever route dropped the line.
|
|
860
|
-
*
|
|
861
|
-
* #2030 had to reach for four things to answer it — a tombstone probe, a
|
|
862
|
-
* visibility set, a byte-range test on `completeIndex`, and a `getNewEntity`
|
|
863
|
-
* fallback whose stated purpose was that `deleteEntity` FORGOT an
|
|
864
|
-
* overlay-created entity instead of tombstoning it, so `isDeleted` could not
|
|
865
|
-
* answer for one. That fallback was documented on main as a workaround for
|
|
866
|
-
* exactly the model-level defect this branch fixes: `deleteEntity` now
|
|
867
|
-
* tombstones as well as forgets, so the effective index answers existence
|
|
868
|
-
* for source and overlay ids alike and the workaround collapses into it.
|
|
869
|
-
*
|
|
870
|
-
* The overlay branch does NOT disappear with it, and the distinction matters:
|
|
871
|
-
* `isOverlayCreated` is still load-bearing here, because a live
|
|
872
|
-
* overlay-created entity has no source bytes and would fail the byte-range
|
|
873
|
-
* test that a source record passes. What the tombstone fix removed is the
|
|
874
|
-
* need for that branch to double as a deletion detector.
|
|
875
|
-
*
|
|
876
|
-
* Deliberately unchanged from #2030 for source records under `deltaOnly` /
|
|
877
|
-
* `exportPropertiesOnly`: the source-iteration pass is skipped wholesale in
|
|
878
|
-
* those modes, yet a source entity still answers true here. A delta is a
|
|
879
|
-
* patch against a file that already has the line, not a standalone model.
|
|
880
|
-
*/
|
|
881
|
-
const willBeEmitted = (entityId) => {
|
|
882
|
-
if (allowedEntityIds !== null && !allowedEntityIds.has(entityId))
|
|
883
|
-
return false;
|
|
884
|
-
// Undefined for a tombstoned id and for one neither the file nor the
|
|
885
|
-
// session ever had — a stale mutation must not conjure a relation either.
|
|
886
|
-
const ref = effective.get(entityId);
|
|
887
|
-
if (!ref)
|
|
888
|
-
return false;
|
|
889
|
-
// An overlay-created record carries the placeholder byte range and is
|
|
890
|
-
// written by the new-entities pass; a source record needs real bytes.
|
|
891
|
-
if (effective.isOverlayCreated(entityId)) {
|
|
892
|
-
// The overlay new-entities pass applies its OWN `isGeometryEntity`
|
|
893
|
-
// filter unconditionally — deltaOnly or not (see the comment at that
|
|
894
|
-
// loop, further below) — so this branch mirrors it without the
|
|
895
|
-
// deltaOnly carve-out the source branch gets.
|
|
896
|
-
return !isGeometryExcluded(entityId, ref.type);
|
|
897
|
-
}
|
|
898
|
-
// Same readability test as `hasEmittableHostBytes`, and for the reason
|
|
899
|
-
// that predicate names: a ref this source cannot address is not a line
|
|
900
|
-
// this export can write, so nothing may be generated naming it (#2491).
|
|
901
|
-
if (!isReadableSourceRef(ref))
|
|
902
|
-
return false;
|
|
903
|
-
// Mirrors `hasEmittableHostBytes`: under `deltaOnly` the source-
|
|
904
|
-
// iteration pass — and its geometry skip — never runs, so a source
|
|
905
|
-
// entity's line is assumed to already exist in the file being patched.
|
|
906
|
-
if (options.deltaOnly === true)
|
|
907
|
-
return true;
|
|
908
|
-
return !isGeometryExcluded(entityId, ref.type);
|
|
909
|
-
};
|
|
910
|
-
// A modified pset is replaced wholesale, which skips ALL of its member atoms.
|
|
911
|
-
// But IFC exporters deduplicate identical Pset_*Common atoms (e.g. one
|
|
912
|
-
// IsExternal IfcPropertySingleValue shared by dozens of psets), so skipping a
|
|
913
|
-
// shared atom would orphan every OTHER pset that still references it, leaving
|
|
914
|
-
// dangling refs and an invalid file. Keep any atom a surviving container needs.
|
|
915
|
-
this.retainSharedAtoms(skipPropertySetIds, allowedEntityIds);
|
|
916
|
-
// Export original entities from source buffer, SKIPPING modified property sets
|
|
917
|
-
if (!options.deltaOnly && this.dataStore.source) {
|
|
918
|
-
const source = this.dataStore.source;
|
|
919
|
-
// Extract existing entities from source. The effective index has already
|
|
920
|
-
// dropped everything the overlay tombstoned, so there is no separate
|
|
921
|
-
// deleted check to forget here.
|
|
922
|
-
for (const [expressId, entityRef] of effective) {
|
|
923
|
-
// Skip overlay-only entities — emitted by the new-entities pass below.
|
|
924
|
-
// A ref this source cannot address is skipped by the same test rather
|
|
925
|
-
// than decoded: `decodeUtf8` clamps such a range and the empty string
|
|
926
|
-
// it returns used to be pushed into the file as a blank line, leaving
|
|
927
|
-
// every generated record that names the host dangling (#2491).
|
|
928
|
-
if (!isReadableSourceRef(entityRef)) {
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
// Skip entities outside the visible closure
|
|
932
|
-
if (allowedEntityIds !== null && !allowedEntityIds.has(expressId)) {
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
// Skip property sets/relationships that are being replaced
|
|
936
|
-
if (skipPropertySetIds.has(expressId) || skipRelationshipIds.has(expressId)) {
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
// Skip type entities whose HasPropertySets attribute will be rewritten
|
|
940
|
-
if (rewrittenEntityIds.has(expressId)) {
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
// Skip if we're only doing geometry or specific types
|
|
944
|
-
const entityType = entityRef.type.toUpperCase();
|
|
945
|
-
// Skip geometry if not included
|
|
946
|
-
if (options.includeGeometry === false && this.isGeometryEntity(entityType)) {
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
// Get original entity text — decodeRange handles SAB-backed
|
|
950
|
-
// sources (Firefox/Chrome reject `TextDecoder.decode()` on a
|
|
951
|
-
// SharedArrayBuffer-backed view; the parser deliberately keeps
|
|
952
|
-
// `source` zero-copy SAB-backed for worker sharing).
|
|
953
|
-
const entityText = decodeRange(source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
954
|
-
// Retype, named attribute edits and positional edits, in that order.
|
|
955
|
-
// Shared verbatim with the type-object `HasPropertySets` rewrite below,
|
|
956
|
-
// which writes the line this pass would otherwise have written.
|
|
957
|
-
const mutated = this.applySourceLineMutations(expressId, entityText, entityRef.type, modifiedAttributes.get(expressId), sourceSchema, overlayActive);
|
|
958
|
-
let nextEntityText = mutated.text;
|
|
959
|
-
// A hidden PRODUCT's own line is already out of the export via
|
|
960
|
-
// `allowedEntityIds`, and a TOMBSTONED entity's via `effective` — this
|
|
961
|
-
// is the relationship that NAMED either one. `IFCREL*` is an
|
|
962
|
-
// unconditional root (see `getVisibleEntityIds`), so its bytes reach
|
|
963
|
-
// here unfiltered even when one of the ids they name was just
|
|
964
|
-
// excluded; left alone that ships a `#N` with no `#N=` line, whether
|
|
965
|
-
// the exclusion came from `visibleOnly` or from a plain deletion
|
|
966
|
-
// (#2398). Checked before the nomination below: a relationship this
|
|
967
|
-
// withholds must not also be counted as a delivered modification.
|
|
968
|
-
//
|
|
969
|
-
// Classified by the EFFECTIVE type (`effective.effectiveType`), not
|
|
970
|
-
// the authored `entityType` above: a retype can move a record across
|
|
971
|
-
// the `IFCREL*` boundary in either direction (`applySourceLineMutations`
|
|
972
|
-
// already rewrote `nextEntityText` to the new class), and this check
|
|
973
|
-
// has to agree with what actually got written, the same way
|
|
974
|
-
// `getVisibleEntityIds` already does for the visibility walk itself.
|
|
975
|
-
const effectiveRelType = effective.effectiveType(expressId, entityRef.type).toUpperCase();
|
|
976
|
-
if (mayNameExcludedRefs && effectiveRelType.startsWith('IFCREL')) {
|
|
977
|
-
const filtered = filterHiddenRefsFromRelationshipLine(nextEntityText, isExcludedFromRelationshipRefs);
|
|
978
|
-
if (filtered === null)
|
|
979
|
-
continue;
|
|
980
|
-
nextEntityText = filtered;
|
|
981
|
-
}
|
|
982
|
-
// A retype or a positional edit that CHANGED the line is what makes
|
|
983
|
-
// this entity count; a named attribute edit was already nominated by
|
|
984
|
-
// the collection pass. Both flags report effect, so retyping an entity
|
|
985
|
-
// to the class it already is — or writing a slot the token it already
|
|
986
|
-
// holds — no longer claims a modification over a line the export left
|
|
987
|
-
// byte-identical. This pass is full-export-only (`deltaOnly` skips it
|
|
988
|
-
// wholesale), so nomination IS emission here and the kinds only have to
|
|
989
|
-
// be right for the entity count — which is per entity, hence unchanged.
|
|
990
|
-
if (mutated.retyped || mutated.positional)
|
|
991
|
-
modifiedEntities.add(expressId);
|
|
992
|
-
if (mutated.retyped)
|
|
993
|
-
modifications.nominate(expressId, 'retype');
|
|
994
|
-
if (mutated.positional)
|
|
995
|
-
modifications.nominate(expressId, 'positional');
|
|
996
|
-
// The named-attribute kinds join them here rather than at their
|
|
997
|
-
// collection sites, for the same reason and on the same signal (#2483).
|
|
998
|
-
// This pass is full-export-only, so there is nothing to gate.
|
|
999
|
-
nominateDeliveredInPlaceEdits(modifications, expressId, mutated, inPlaceNominees);
|
|
1000
|
-
// Apply schema conversion if exporting to a different schema version
|
|
1001
|
-
if (converting) {
|
|
1002
|
-
const converted = convertStepLine(nextEntityText, sourceSchema, schema, options.guidRandom);
|
|
1003
|
-
if (converted !== null) {
|
|
1004
|
-
entities.push(converted);
|
|
1005
|
-
}
|
|
1006
|
-
// null means entity should be skipped (no valid representation in target schema)
|
|
1007
|
-
}
|
|
1008
|
-
else {
|
|
1009
|
-
entities.push(nextEntityText);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
// Generate new property entities for mutations (these REPLACE the skipped ones)
|
|
1014
|
-
const generatedTypeOwnedPsetIds = new Map();
|
|
1015
|
-
for (const { entityId, psets } of newPropertySets) {
|
|
1016
|
-
// Nothing may be emitted FOR an entity that gets no defining line —
|
|
1017
|
-
// see `willBeEmitted` (#1978, #2030, #2012).
|
|
1018
|
-
if (!willBeEmitted(entityId))
|
|
1019
|
-
continue;
|
|
1020
|
-
const newEntities = this.generatePropertySetEntities(entityId, psets, willBeEmitted, effective, typeOwnedPsetNamesByEntity.get(entityId), options.guidRandom);
|
|
1021
|
-
entities.push(...newEntities.lines);
|
|
1022
|
-
newEntityCount += newEntities.count;
|
|
1023
|
-
// Replacement content for this host actually landed, so a delta really
|
|
1024
|
-
// does carry its PROPERTY-SET modification — and only that one (#2462).
|
|
1025
|
-
if (newEntities.lines.length > 0)
|
|
1026
|
-
modifications.recordEmitted(entityId, 'property-set');
|
|
1027
|
-
generatedTypeOwnedPsetIds.set(entityId, newEntities.generatedTypeOwnedPsetIds);
|
|
1028
|
-
}
|
|
1029
|
-
// Point every affected type object's HasPropertySets at the psets this
|
|
1030
|
-
// export generated. One loop, because a type whose affected psets produced
|
|
1031
|
-
// no replacement content (a deletion) needs exactly the same resolution
|
|
1032
|
-
// with an empty replacement map.
|
|
1033
|
-
for (const [entityId, typeOwnedPsetNames] of typeOwnedPsetNamesByEntity) {
|
|
1034
|
-
// `entityId` here is a TYPE object rather than an element; `willBeEmitted`
|
|
1035
|
-
// resolves either the same way (#2030).
|
|
1036
|
-
if (!willBeEmitted(entityId))
|
|
1037
|
-
continue;
|
|
1038
|
-
const resolved = resolveTypeOwnedPsetIds(typeOwnedPsetIdsByEntity.get(entityId) ?? [], typeOwnedPsetNames, generatedTypeOwnedPsetIds.get(entityId) ?? new Map(), (psetId) => this.getPropertySetName(psetId));
|
|
1039
|
-
if (effective.isOverlayCreated(entityId)) {
|
|
1040
|
-
// No source line to rewrite: the new-entities pass writes this record
|
|
1041
|
-
// from its authored payload, so the list rides in as a slot override.
|
|
1042
|
-
overlayTypeOwnedPsets.set(entityId, resolved.length > 0 ? resolved.map((id) => `#${id}`) : null);
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
// This line REPLACES the one the source-iteration pass would have
|
|
1046
|
-
// written — `rewrittenEntityIds` makes that pass skip the entity — so it
|
|
1047
|
-
// has to carry the entity's other edits too, and it has to apply them
|
|
1048
|
-
// the way that pass does. It used to replace slot 5 and nothing else,
|
|
1049
|
-
// which dropped the rename in `setAttribute(id,'Name',…)` +
|
|
1050
|
-
// `addPropertySet(id,…)`, and then, once renames were special-cased
|
|
1051
|
-
// here, still dropped retypes and positional edits — same line, same
|
|
1052
|
-
// silence. So run the ONE pipeline both passes share and replace
|
|
1053
|
-
// `HasPropertySets` on its output. Order matters: see
|
|
1054
|
-
// {@link applySourceLineMutations}.
|
|
1055
|
-
const record = effective.get(entityId);
|
|
1056
|
-
let sourceLine = null;
|
|
1057
|
-
let mutated = null;
|
|
1058
|
-
// One narrowed block for both calls: `record` is in scope for the decode
|
|
1059
|
-
// AND for the record type below, with no non-null assertion to keep true
|
|
1060
|
-
// by hand. `byteOffset >= 0` is the same "are there real source bytes"
|
|
1061
|
-
// test the source-iteration pass makes — an overlay-authored record
|
|
1062
|
-
// carries `-1` there, and decoding from it would read another entity's
|
|
1063
|
-
// bytes rather than fall through to the no-source-bytes branch.
|
|
1064
|
-
// `isReadableSourceRef` folds in the `byteOffset >= 0 && byteLength > 0`
|
|
1065
|
-
// test this used to make by hand, and adds the bound the invariant used
|
|
1066
|
-
// to supply (#2491).
|
|
1067
|
-
if (record && isReadableSourceRef(record)) {
|
|
1068
|
-
sourceLine = decodeRange(this.dataStore.source, record.byteOffset, record.byteOffset + record.byteLength);
|
|
1069
|
-
// The RECORD's class is the from-type: the bytes are still the source
|
|
1070
|
-
// class, whatever `typeOf` now says the entity effectively is.
|
|
1071
|
-
mutated = this.applySourceLineMutations(entityId, sourceLine, record.type, modifiedAttributes.get(entityId), sourceSchema, overlayActive);
|
|
1072
|
-
}
|
|
1073
|
-
if (mutated === null) {
|
|
1074
|
-
// `willBeEmitted` already required real source bytes for a non-overlay
|
|
1075
|
-
// record, so this is only reachable with no source buffer at all —
|
|
1076
|
-
// in which case the source-iteration pass never ran either and there is
|
|
1077
|
-
// nothing to lose. Say it anyway; the pset edit is still going nowhere.
|
|
1078
|
-
warnings.push(typeOwnedPsetRewriteWarning(entityId, 'no-source-bytes'));
|
|
1079
|
-
// The line above IS the report, so the ledger must not add a second,
|
|
1080
|
-
// vaguer one blaming the delta format for a drop the format did not
|
|
1081
|
-
// cause.
|
|
1082
|
-
modifications.acknowledgeUndelivered(entityId, 'property-set');
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
1085
|
-
const { line, repointed } = rewriteTypeOwnedPsetLine(mutated.text, resolved);
|
|
1086
|
-
if (repointed) {
|
|
1087
|
-
// A repoint that resolves to the list the line ALREADY names changes
|
|
1088
|
-
// nothing, and it is reachable: deleting a pset name the type object
|
|
1089
|
-
// does not own leaves every original id in place (it is "affected" but
|
|
1090
|
-
// matches none of them) and generates no replacement, so slot 5 comes
|
|
1091
|
-
// back byte-identical. Same rule as the fallback branch below — an
|
|
1092
|
-
// unchanged line has no place in a delta, and claiming it delivered the
|
|
1093
|
-
// edit would put a modification in the header over a line that carries
|
|
1094
|
-
// none. A FULL export still emits it: `rewrittenEntityIds` made the
|
|
1095
|
-
// source-iteration pass skip this entity, so withholding the line there
|
|
1096
|
-
// would delete the record from the file (#2469).
|
|
1097
|
-
const changed = line !== sourceLine;
|
|
1098
|
-
if (options.deltaOnly !== true || changed) {
|
|
1099
|
-
rewrittenEntityLines.set(entityId, line);
|
|
1100
|
-
}
|
|
1101
|
-
// A rewritten source line IS in the delta — the one in-place change a
|
|
1102
|
-
// delta does carry today (#2462). The repoint itself delivers the
|
|
1103
|
-
// property-set edit that put this host in the loop; the rest of the
|
|
1104
|
-
// line delivers whichever in-place edits the pipeline applied to it.
|
|
1105
|
-
if (changed) {
|
|
1106
|
-
modifications.recordEmitted(entityId, 'property-set');
|
|
1107
|
-
recordSourceLineDelivery(modifications, entityId, mutated);
|
|
1108
|
-
// `rewrittenEntityIds` made the source-iteration pass skip this
|
|
1109
|
-
// host, so this line is the ONLY place a full export can see its
|
|
1110
|
-
// named-attribute edits land — per site, not per feature (#2483).
|
|
1111
|
-
nominateDeliveredInPlaceEdits(modifications, entityId, mutated, inPlaceNominees);
|
|
1112
|
-
}
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
// A malformed source line — too few arguments to have a slot 5, or not
|
|
1116
|
-
// parseable as a STEP record at all. The entity must still come out:
|
|
1117
|
-
// `rewrittenEntityIds` made the source-iteration pass skip it, so
|
|
1118
|
-
// dropping the line here deletes the whole record from the file (#2469).
|
|
1119
|
-
warnings.push(typeOwnedPsetRewriteWarning(entityId, 'unparseable-line'));
|
|
1120
|
-
// Same as the `no-source-bytes` branch: the property-set edit is
|
|
1121
|
-
// genuinely undelivered — the repoint is what would have delivered it and
|
|
1122
|
-
// it did not happen — but this warning already says so, precisely, so the
|
|
1123
|
-
// ledger stays quiet about that pair rather than duplicating it. (When
|
|
1124
|
-
// the affected psets produced replacement content, the property-set pass
|
|
1125
|
-
// above has already recorded the emission, and an emission outranks an
|
|
1126
|
-
// acknowledgement.)
|
|
1127
|
-
modifications.acknowledgeUndelivered(entityId, 'property-set');
|
|
1128
|
-
// `line` is byte-for-byte what the source-iteration pass would have
|
|
1129
|
-
// written, so emit it wherever that pass would have run. Under
|
|
1130
|
-
// `deltaOnly` it does not run, and a line the mutation pipeline left
|
|
1131
|
-
// identical to its source is not a change — it has no place in a delta.
|
|
1132
|
-
const changed = line !== sourceLine;
|
|
1133
|
-
if (options.deltaOnly !== true || changed) {
|
|
1134
|
-
rewrittenEntityLines.set(entityId, line);
|
|
1135
|
-
}
|
|
1136
|
-
// The ledger stays honest about WHICH modification landed: the
|
|
1137
|
-
// property-set edit that nominated this host is the thing that just
|
|
1138
|
-
// failed, so only the entity's OTHER edits are in this line. Under the
|
|
1139
|
-
// per-kind keying that comes out as `attribute/retype/positional:
|
|
1140
|
-
// delivered, property-set: undelivered` — the host still counts once,
|
|
1141
|
-
// because a real change of its did land.
|
|
1142
|
-
if (changed) {
|
|
1143
|
-
recordSourceLineDelivery(modifications, entityId, mutated);
|
|
1144
|
-
// Same site rule as the repoint branch above: the failed repoint is
|
|
1145
|
-
// what did not land, and the line still carries the host's OTHER edits.
|
|
1146
|
-
nominateDeliveredInPlaceEdits(modifications, entityId, mutated, inPlaceNominees);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
// Generate new quantity entities for mutations
|
|
1150
|
-
for (const { entityId, qsets } of newQuantitySets) {
|
|
1151
|
-
if (!willBeEmitted(entityId))
|
|
1152
|
-
continue;
|
|
1153
|
-
const newEntities = this.generateQuantitySetEntities(entityId, qsets, willBeEmitted, options.guidRandom);
|
|
1154
|
-
entities.push(...newEntities.lines);
|
|
1155
|
-
newEntityCount += newEntities.count;
|
|
1156
|
-
if (newEntities.lines.length > 0)
|
|
1157
|
-
modifications.recordEmitted(entityId, 'quantity-set');
|
|
1158
|
-
}
|
|
1159
|
-
for (const rewrittenLine of rewrittenEntityLines.values()) {
|
|
1160
|
-
entities.push(rewrittenLine);
|
|
109
|
+
// What comes back is the object the phases below MUTATE -- notably
|
|
110
|
+
// `collectModifications`, which fills in the `allowedEntityIds` /
|
|
111
|
+
// `hiddenProductIds` that the pass's own predicates close over. Do not
|
|
112
|
+
// copy it; `step-pass-builder.test.ts` is what stops that.
|
|
113
|
+
const pass = buildExportPass({
|
|
114
|
+
dataStore: this.dataStore,
|
|
115
|
+
mutationView: this.mutationView,
|
|
116
|
+
isGeometryEntity,
|
|
117
|
+
options,
|
|
118
|
+
schema,
|
|
119
|
+
sourceSchema,
|
|
120
|
+
converting,
|
|
121
|
+
applyMutations,
|
|
122
|
+
excludeGeometry,
|
|
123
|
+
sourceHeader,
|
|
124
|
+
schemaToken,
|
|
125
|
+
});
|
|
126
|
+
// Visible-only closure, overlay mutation grouping, and georeferencing
|
|
127
|
+
// edits — everything `pass` needs before the omission predicates below,
|
|
128
|
+
// and before the output passes that consume them, can run (#2475, the
|
|
129
|
+
// collection block).
|
|
130
|
+
collectModifications(pass, options, applyMutations, this.collectionContext());
|
|
131
|
+
// Which ids this export may still NAME, now that the collection phase has
|
|
132
|
+
// decided which ids it is WRITING (#2475). Must stay here, between those
|
|
133
|
+
// two -- see `step-omission-predicates.ts` for why the position is load
|
|
134
|
+
// bearing rather than stylistic.
|
|
135
|
+
const omission = evaluateOmissionPredicates(pass, options, applyMutations, excludeGeometry, this.mutationView);
|
|
136
|
+
// A deltaOnly export with nothing to say is already finished.
|
|
137
|
+
if (omission.kind === 'short-circuit')
|
|
138
|
+
return omission.result;
|
|
139
|
+
const { isOmittedFromOutput, mayNameOmittedRefs } = omission;
|
|
140
|
+
// Write every source-backed record this export keeps (#2475 step 2d),
|
|
141
|
+
// preceded — inside that call — by the shared-atom retention that decides
|
|
142
|
+
// which member atoms the skip sets may still drop.
|
|
143
|
+
writeSourceEntityLines(pass, options, mayNameOmittedRefs, isOmittedFromOutput, buildSourceIterationContext(this.dataStore, this.mutationView, () => this.propertySetContext()));
|
|
144
|
+
// Generated property/quantity sets and the type-object `HasPropertySets`
|
|
145
|
+
// rewrite that resolves against them, in that one order (#2475 steps 2b
|
|
146
|
+
// and 2c). `pass.rewrittenEntityLines`, this call's output, is flushed
|
|
147
|
+
// just below — after the quantity-set loop inside it, as it always was.
|
|
148
|
+
generatePropertyAndQuantitySetEntities(pass, options, this.propertySetContext());
|
|
149
|
+
for (const rewrittenLine of pass.rewrittenEntityLines.values()) {
|
|
150
|
+
pass.entities.push(rewrittenLine);
|
|
1161
151
|
}
|
|
1162
152
|
// Add new georeferencing entities (IfcProjectedCRS, IfcMapConversion)
|
|
1163
|
-
for (const line of newGeorefLines) {
|
|
1164
|
-
entities.push(line);
|
|
1165
|
-
}
|
|
1166
|
-
// Add overlay-created entities (store.addEntity / mutationView.createEntity).
|
|
1167
|
-
// Apply the same filters as the source-iteration pass so newly-created
|
|
1168
|
-
// beams/slabs don't smuggle their geometry helpers (IfcCartesianPoint,
|
|
1169
|
-
// IfcExtrudedAreaSolid, etc.) past `includeGeometry:false` /
|
|
1170
|
-
// `exportPropertiesOnly()` modes.
|
|
1171
|
-
if (this.mutationView
|
|
1172
|
-
&& (options.applyMutations !== false)
|
|
1173
|
-
&& typeof this.mutationView.getNewEntities === 'function') {
|
|
1174
|
-
const getTypeMut = typeof this.mutationView.getEntityTypeMutation === 'function'
|
|
1175
|
-
? this.mutationView.getEntityTypeMutation.bind(this.mutationView)
|
|
1176
|
-
: null;
|
|
1177
|
-
for (const entity of this.mutationView.getNewEntities()) {
|
|
1178
|
-
// A retyped overlay entity keeps its AUTHORED type on `entity.type`
|
|
1179
|
-
// (the overlay typeMutation is the source of truth for the effective
|
|
1180
|
-
// class). Resolve the effective class, then re-lay-out the authored
|
|
1181
|
-
// attributes from the authored layout up to it.
|
|
1182
|
-
const typeMut = getTypeMut ? getTypeMut(entity.expressId) : null;
|
|
1183
|
-
const effectiveType = typeMut?.newType ?? entity.type;
|
|
1184
|
-
// STEP requires UPPERCASE entity type tokens; the upper-case happens
|
|
1185
|
-
// here at the file-format boundary.
|
|
1186
|
-
const upperType = effectiveType.toUpperCase();
|
|
1187
|
-
if (options.includeGeometry === false && this.isGeometryEntity(upperType)) {
|
|
1188
|
-
continue;
|
|
1189
|
-
}
|
|
1190
|
-
if (allowedEntityIds !== null && !allowedEntityIds.has(entity.expressId)) {
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1193
|
-
// Re-lay-out by name against the effective class (identity for
|
|
1194
|
-
// compatible layouts). Runs whenever a retype intent exists — even a
|
|
1195
|
-
// same-class retype, which carries a PredefinedType override
|
|
1196
|
-
// (e.g. setEntityType(id, 'IfcColumn', 'PILASTER')).
|
|
1197
|
-
let argsText;
|
|
1198
|
-
if (typeMut) {
|
|
1199
|
-
// Serialize against the AUTHORED layout (`entity.type`); retypeArgTokens
|
|
1200
|
-
// then re-lays the tokens out by name up to the effective class.
|
|
1201
|
-
const srcTokens = entity.attributes.map((value, i) => serializeAttributeSlot(entity.type, i, value, sourceSchema));
|
|
1202
|
-
const { tokens } = retypeArgTokens(srcTokens, entity.type, effectiveType, typeMut.predefinedType ?? null, sourceSchema);
|
|
1203
|
-
argsText = tokens.join(',');
|
|
1204
|
-
}
|
|
1205
|
-
else {
|
|
1206
|
-
argsText = serializeEntityArgs(entity.type, entity.attributes, sourceSchema);
|
|
1207
|
-
}
|
|
1208
|
-
// Edits made AFTER the create live in the overlay, never in the
|
|
1209
|
-
// authored payload (#2006). The source-iteration pass applies them to
|
|
1210
|
-
// source records via applyAttributeMutations / applyPositionalMutations;
|
|
1211
|
-
// an overlay-created entity has no source record, so without this it was
|
|
1212
|
-
// written from its creation payload alone and every later
|
|
1213
|
-
// `setAttribute` / `setPositionalAttribute` was silently dropped on
|
|
1214
|
-
// save — data loss with no error and no warning.
|
|
1215
|
-
//
|
|
1216
|
-
// Order mirrors the source pass: retype (above) -> named attributes ->
|
|
1217
|
-
// positional overrides, all resolved against the EFFECTIVE class.
|
|
1218
|
-
const attributeOverrides = modifiedAttributes.get(entity.expressId) ?? null;
|
|
1219
|
-
const queuedPositional = typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
1220
|
-
? this.mutationView.getPositionalMutationsForEntity(entity.expressId)
|
|
1221
|
-
: null;
|
|
1222
|
-
// A created TYPE object owns its psets through HasPropertySets, and the
|
|
1223
|
-
// ids of the psets this export generated are only known now — so they
|
|
1224
|
-
// arrive as one more slot override rather than through the overlay.
|
|
1225
|
-
// `has`, not `??`, for the same reason `overlaySlotValue` gives: the
|
|
1226
|
-
// stored value is deliberately null when the resolved list is empty.
|
|
1227
|
-
const positionalOverrides = overlayTypeOwnedPsets.has(entity.expressId)
|
|
1228
|
-
? new Map(queuedPositional).set(HAS_PROPERTY_SETS_SLOT, overlayTypeOwnedPsets.get(entity.expressId) ?? null)
|
|
1229
|
-
: queuedPositional;
|
|
1230
|
-
if ((attributeOverrides && attributeOverrides.size > 0)
|
|
1231
|
-
|| (positionalOverrides && positionalOverrides.size > 0)) {
|
|
1232
|
-
argsText = this.applyOverlayEntityOverrides(argsText, upperType, attributeOverrides, positionalOverrides, sourceSchema);
|
|
1233
|
-
}
|
|
1234
|
-
let line = `#${entity.expressId}=${upperType}(${argsText});`;
|
|
1235
|
-
// Same gap as the source-iteration pass, for an overlay-authored
|
|
1236
|
-
// relationship instead of a parsed one (#2398).
|
|
1237
|
-
if (mayNameExcludedRefs && upperType.startsWith('IFCREL')) {
|
|
1238
|
-
line = filterHiddenRefsFromRelationshipLine(line, isExcludedFromRelationshipRefs);
|
|
1239
|
-
if (line === null)
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
if (converting) {
|
|
1243
|
-
const converted = convertStepLine(line, sourceSchema, schema, options.guidRandom);
|
|
1244
|
-
if (converted !== null) {
|
|
1245
|
-
entities.push(converted);
|
|
1246
|
-
newEntityCount++;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
else {
|
|
1250
|
-
entities.push(line);
|
|
1251
|
-
newEntityCount++;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
153
|
+
for (const line of pass.newGeorefLines) {
|
|
154
|
+
pass.entities.push(line);
|
|
1254
155
|
}
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1257
|
-
|
|
1258
|
-
//
|
|
1259
|
-
//
|
|
1260
|
-
|
|
1261
|
-
warnings.push(...deltaWarnings);
|
|
1262
|
-
// Assemble final file as Uint8Array chunks to avoid V8 string length limit.
|
|
1263
|
-
// The header is built last so its provenance item reflects the real count.
|
|
1264
|
-
const header = buildHeader(newEntityCount + modifiedEntityCount);
|
|
1265
|
-
const content = assembleStepBytes(header, entities);
|
|
1266
|
-
return {
|
|
1267
|
-
content,
|
|
1268
|
-
stats: {
|
|
1269
|
-
entityCount: entities.length,
|
|
1270
|
-
newEntityCount,
|
|
1271
|
-
modifiedEntityCount,
|
|
1272
|
-
fileSize: content.byteLength,
|
|
1273
|
-
warnings,
|
|
1274
|
-
},
|
|
1275
|
-
};
|
|
156
|
+
// Add overlay-created entities (store.addEntity / mutationView.createEntity),
|
|
157
|
+
// applying the same filters as the source-iteration pass (#2475 step 2e).
|
|
158
|
+
writeOverlayCreatedEntities(pass, options, excludeGeometry, applyMutations, mayNameOmittedRefs, isOmittedFromOutput, buildOverlayEntitiesContext(this.mutationView));
|
|
159
|
+
// Settle the ledger, build the header, assemble the finished bytes —
|
|
160
|
+
// `step-header.ts` (#2475 header/assembly tail).
|
|
161
|
+
return assembleExportResult(pass);
|
|
1276
162
|
}
|
|
1277
163
|
/**
|
|
1278
164
|
* Async export that yields to the event loop periodically, keeping the
|
|
@@ -1306,573 +192,6 @@ export class StepExporter {
|
|
|
1306
192
|
deltaOnly: true,
|
|
1307
193
|
});
|
|
1308
194
|
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Resolve a STEP reference to an existing IfcOwnerHistory for the
|
|
1311
|
-
* IfcPropertySet / IfcRelDefinesByProperties / IfcElementQuantity entities we
|
|
1312
|
-
* generate for `hostEntityId`'s mutations. OwnerHistory is optional in IFC4 but
|
|
1313
|
-
* MANDATORY in IFC2X3 (IfcRoot.OwnerHistory), so emitting `$` yields an invalid
|
|
1314
|
-
* IFC2X3 file that strict readers (e.g. BIM Vision) reject.
|
|
1315
|
-
*
|
|
1316
|
-
* Prefer the host element's OWN owner history, then any owner history that
|
|
1317
|
-
* survives this export, then `$` only when none does.
|
|
1318
|
-
*
|
|
1319
|
-
* "Survives" is `willBeEmitted`, the same predicate that decides whether the
|
|
1320
|
-
* host itself may have psets generated for it. A reference is a reference: it
|
|
1321
|
-
* is no more acceptable to point an emitted `IfcPropertySet` at an owner
|
|
1322
|
-
* history the session deleted than at a host it deleted. This used to consult
|
|
1323
|
-
* only the `visibleOnly` closure, so an overlay-created OwnerHistory that was
|
|
1324
|
-
* later deleted still got referenced — a dangling `#N`, reached through the
|
|
1325
|
-
* one attribute the generators fill in for themselves.
|
|
1326
|
-
*/
|
|
1327
|
-
resolveOwnerHistoryRef(hostEntityId, willBeEmitted) {
|
|
1328
|
-
const own = this.getOwnerHistoryRefOfEntity(hostEntityId);
|
|
1329
|
-
if (own !== null) {
|
|
1330
|
-
const ownId = parseInt(own.slice(1), 10);
|
|
1331
|
-
if (willBeEmitted(ownId))
|
|
1332
|
-
return own;
|
|
1333
|
-
}
|
|
1334
|
-
if (this.ownerHistoryFallbackRef === undefined) {
|
|
1335
|
-
// Source-only: the fallback is a best-effort "some owner history the file
|
|
1336
|
-
// still has", and the host's OWN history above is the path that resolves
|
|
1337
|
-
// an overlay-created one.
|
|
1338
|
-
const ids = this.dataStore.entityIndex.byType.get('IFCOWNERHISTORY') ?? [];
|
|
1339
|
-
const surviving = ids.find((id) => willBeEmitted(id));
|
|
1340
|
-
this.ownerHistoryFallbackRef = surviving !== undefined ? `#${surviving}` : '$';
|
|
1341
|
-
}
|
|
1342
|
-
return this.ownerHistoryFallbackRef;
|
|
1343
|
-
}
|
|
1344
|
-
/**
|
|
1345
|
-
* The overlay's answer for one positional slot of an overlay-created entity,
|
|
1346
|
-
* falling back to the creation payload only when the overlay has NOTHING to
|
|
1347
|
-
* say about that slot.
|
|
1348
|
-
*
|
|
1349
|
-
* **Ask `Map.has`, never `??`.** `setPositionalAttribute(id, slot, null)` is
|
|
1350
|
-
* an explicit "clear this slot", and its value is `null`, so `??` reads the
|
|
1351
|
-
* overlay's answer as an absence and reinstates the authored one. That is the
|
|
1352
|
-
* same overlay-versus-buffer confusion this whole change is about, one
|
|
1353
|
-
* attribute wide: an explicit null IS the overlay's answer, and the overlay is
|
|
1354
|
-
* the authority. Cleared OwnerHistory came back as the authored reference, and
|
|
1355
|
-
* a cleared `HasPropertySets` resurrected the list the user had removed.
|
|
1356
|
-
*/
|
|
1357
|
-
overlaySlotValue(entityId, slot, authored) {
|
|
1358
|
-
const overrides = this.mutationView?.getPositionalMutationsForEntity(entityId);
|
|
1359
|
-
if (!overrides?.has(slot))
|
|
1360
|
-
return authored;
|
|
1361
|
-
const value = overrides.get(slot);
|
|
1362
|
-
// `Map.get` widens to `| undefined`, which `has` has already ruled out. A
|
|
1363
|
-
// slot explicitly set to nothing serializes as `$`, i.e. null.
|
|
1364
|
-
return value === undefined ? null : value;
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Read an element's own OwnerHistory reference (`#id`), or null when the
|
|
1368
|
-
* element omits one (`$`) or cannot be parsed. OwnerHistory is the second
|
|
1369
|
-
* attribute of every IfcRoot subtype, immediately after the GlobalId string.
|
|
1370
|
-
*/
|
|
1371
|
-
getOwnerHistoryRefOfEntity(entityId) {
|
|
1372
|
-
const cached = this.ownerHistoryByEntity.get(entityId);
|
|
1373
|
-
if (cached !== undefined)
|
|
1374
|
-
return cached;
|
|
1375
|
-
let result = null;
|
|
1376
|
-
// An overlay-created host has no source line to read, but it does have an
|
|
1377
|
-
// authored OwnerHistory in slot 1 — reading only the buffer sent every
|
|
1378
|
-
// generated pset on a created entity to the file's first owner history
|
|
1379
|
-
// instead of the one the caller named (#2012).
|
|
1380
|
-
const overlay = this.mutationView?.getNewEntity(entityId);
|
|
1381
|
-
if (overlay) {
|
|
1382
|
-
const refs = authoredEntityRefs(this.overlaySlotValue(entityId, OWNER_HISTORY_SLOT, overlay.attributes[OWNER_HISTORY_SLOT]));
|
|
1383
|
-
result = refs.length > 0 ? `#${refs[0]}` : null;
|
|
1384
|
-
this.ownerHistoryByEntity.set(entityId, result);
|
|
1385
|
-
return result;
|
|
1386
|
-
}
|
|
1387
|
-
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
1388
|
-
// Readability rather than presence, as everywhere else (#2491). A clamped
|
|
1389
|
-
// decode would match nothing here, so this is tidiness rather than a bug —
|
|
1390
|
-
// but the gates in this file agree on one predicate now.
|
|
1391
|
-
if (entityRef && this.isReadableSourceRef(entityRef)) {
|
|
1392
|
-
const entityText = decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
1393
|
-
// #ID=IFCWALL('GlobalId',#owner,...): GlobalId is a quoted STEP string
|
|
1394
|
-
// (doubled '' escapes); OwnerHistory is the ref/`$` right after it.
|
|
1395
|
-
const match = entityText.match(/=\s*IFC\w+\s*\(\s*'(?:[^']|'')*'\s*,\s*#(\d+)/i);
|
|
1396
|
-
if (match)
|
|
1397
|
-
result = `#${match[1]}`;
|
|
1398
|
-
}
|
|
1399
|
-
this.ownerHistoryByEntity.set(entityId, result);
|
|
1400
|
-
return result;
|
|
1401
|
-
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Generate STEP entities for property sets
|
|
1404
|
-
*/
|
|
1405
|
-
generatePropertySetEntities(entityId, psets, willBeEmitted, effective, typeOwnedPsetNames, random) {
|
|
1406
|
-
const lines = [];
|
|
1407
|
-
let count = 0;
|
|
1408
|
-
const generatedTypeOwnedPsetIds = new Map();
|
|
1409
|
-
for (const pset of psets) {
|
|
1410
|
-
const propertyIds = [];
|
|
1411
|
-
// Create IfcPropertySingleValue for each property
|
|
1412
|
-
for (const prop of pset.properties) {
|
|
1413
|
-
const propId = this.nextExpressId++;
|
|
1414
|
-
count++;
|
|
1415
|
-
// `prop.dataType`, not `prop.type` alone: regenerating the set rewrites
|
|
1416
|
-
// every property in it, and the shape-derived primitive would re-declare
|
|
1417
|
-
// the ones nobody edited (`IFCTEXT` → `IFCLABEL`, `IFCLENGTHMEASURE` →
|
|
1418
|
-
// `IFCREAL`). See `declared-property-type.ts` for when the source token
|
|
1419
|
-
// is trusted (#2482).
|
|
1420
|
-
const valueStr = serializeNominalValue(prop.value, prop.type, prop.dataType);
|
|
1421
|
-
const unitId = prop.unit ? this.findUnitId(prop.unit, effective) : null;
|
|
1422
|
-
const unitStr = unitId !== null ? ref(unitId) : null;
|
|
1423
|
-
// #ID=IFCPROPERTYSINGLEVALUE('Name',$,Value,Unit);
|
|
1424
|
-
const line = `#${propId}=IFCPROPERTYSINGLEVALUE('${escapeStepString(prop.name)}',$,${valueStr},${unitStr ? serializeValue(unitStr) : '$'});`;
|
|
1425
|
-
lines.push(line);
|
|
1426
|
-
propertyIds.push(propId);
|
|
1427
|
-
}
|
|
1428
|
-
// Create IfcPropertySet
|
|
1429
|
-
const psetId = this.nextExpressId++;
|
|
1430
|
-
count++;
|
|
1431
|
-
const propRefs = propertyIds.map(id => `#${id}`).join(',');
|
|
1432
|
-
const globalId = this.generateGlobalId(random);
|
|
1433
|
-
// #ID=IFCPROPERTYSET('GlobalId',#ownerHistory,'Name',$,(#props));
|
|
1434
|
-
const psetLine = `#${psetId}=IFCPROPERTYSET('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(pset.name)}',$,(${propRefs}));`;
|
|
1435
|
-
lines.push(psetLine);
|
|
1436
|
-
if (typeOwnedPsetNames?.has(pset.name)) {
|
|
1437
|
-
generatedTypeOwnedPsetIds.set(pset.name, psetId);
|
|
1438
|
-
}
|
|
1439
|
-
else {
|
|
1440
|
-
// Create IfcRelDefinesByProperties to link pset to entity
|
|
1441
|
-
const relId = this.nextExpressId++;
|
|
1442
|
-
count++;
|
|
1443
|
-
const relGlobalId = this.generateGlobalId(random);
|
|
1444
|
-
// #ID=IFCRELDEFINESBYPROPERTIES('GlobalId',#ownerHistory,$,$,(#entity),#pset);
|
|
1445
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${psetId});`;
|
|
1446
|
-
lines.push(relLine);
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return { lines, count, generatedTypeOwnedPsetIds };
|
|
1450
|
-
}
|
|
1451
|
-
/**
|
|
1452
|
-
* Generate STEP entities for quantity sets (IfcElementQuantity)
|
|
1453
|
-
*/
|
|
1454
|
-
generateQuantitySetEntities(entityId, qsets, willBeEmitted, random) {
|
|
1455
|
-
const lines = [];
|
|
1456
|
-
let count = 0;
|
|
1457
|
-
for (const qset of qsets) {
|
|
1458
|
-
const quantityIds = [];
|
|
1459
|
-
for (const q of qset.quantities) {
|
|
1460
|
-
const qId = this.nextExpressId++;
|
|
1461
|
-
count++;
|
|
1462
|
-
const ifcType = quantityTypeToIfcType(q.type);
|
|
1463
|
-
// #ID=IFCQUANTITYLENGTH('Name',$,$,Value,$);
|
|
1464
|
-
const val = toStepReal(q.value);
|
|
1465
|
-
const line = `#${qId}=${ifcType}('${escapeStepString(q.name)}',$,$,${val},$);`;
|
|
1466
|
-
lines.push(line);
|
|
1467
|
-
quantityIds.push(qId);
|
|
1468
|
-
}
|
|
1469
|
-
// Create IfcElementQuantity
|
|
1470
|
-
const qsetId = this.nextExpressId++;
|
|
1471
|
-
count++;
|
|
1472
|
-
const quantRefs = quantityIds.map(id => `#${id}`).join(',');
|
|
1473
|
-
const globalId = this.generateGlobalId(random);
|
|
1474
|
-
// #ID=IFCELEMENTQUANTITY('GlobalId',#ownerHistory,'Name',$,$,(#quants));
|
|
1475
|
-
const qsetLine = `#${qsetId}=IFCELEMENTQUANTITY('${globalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},'${escapeStepString(qset.name)}',$,$,(${quantRefs}));`;
|
|
1476
|
-
lines.push(qsetLine);
|
|
1477
|
-
// Create IfcRelDefinesByProperties to link qset to entity
|
|
1478
|
-
const relId = this.nextExpressId++;
|
|
1479
|
-
count++;
|
|
1480
|
-
const relGlobalId = this.generateGlobalId(random);
|
|
1481
|
-
const relLine = `#${relId}=IFCRELDEFINESBYPROPERTIES('${relGlobalId}',${this.resolveOwnerHistoryRef(entityId, willBeEmitted)},$,$,(#${entityId}),#${qsetId});`;
|
|
1482
|
-
lines.push(relLine);
|
|
1483
|
-
}
|
|
1484
|
-
return { lines, count };
|
|
1485
|
-
}
|
|
1486
|
-
/**
|
|
1487
|
-
* THE mutation pipeline for a line read out of the source buffer: retype,
|
|
1488
|
-
* then named attribute edits, then positional edits.
|
|
1489
|
-
*
|
|
1490
|
-
* **One implementation, two call sites**, and that is the whole point. Two
|
|
1491
|
-
* passes can write the defining line of a source entity — the
|
|
1492
|
-
* source-iteration pass, and the type-object `HasPropertySets` rewrite that
|
|
1493
|
-
* REPLACES it (`rewrittenEntityIds` makes the source pass skip those ids).
|
|
1494
|
-
* The rewrite used to do its own thing (replace slot 5, nothing else), so
|
|
1495
|
-
* every other edit to a type object with a type-owned pset edit was dropped
|
|
1496
|
-
* in silence: first the renames (#2462 follow-up), and after those were
|
|
1497
|
-
* special-cased here, still the retypes and the positional edits. Whatever
|
|
1498
|
-
* the source pass applies, the rewrite has to apply too, or the next edit
|
|
1499
|
-
* kind added to one site goes missing at the other.
|
|
1500
|
-
*
|
|
1501
|
-
* The order is load-bearing:
|
|
1502
|
-
*
|
|
1503
|
-
* - the retype runs FIRST so named attribute edits resolve against the
|
|
1504
|
-
* TARGET class's attribute names, and so positional slots are indexed
|
|
1505
|
-
* into the retyped argument list;
|
|
1506
|
-
* - the `HasPropertySets` replacement (rewrite path only) runs LAST, on
|
|
1507
|
-
* the text this returns. Run it first and a positional edit to slot 5 —
|
|
1508
|
-
* or a retype's argument-list rebuild — overwrites the resolved pset
|
|
1509
|
-
* list with the stale one, which is the same silent drop one slot over.
|
|
1510
|
-
*
|
|
1511
|
-
* The expressId is unchanged by all of this, so geometry / placement /
|
|
1512
|
-
* representation and every IfcRel* reference (keyed by #id) carry over.
|
|
1513
|
-
*
|
|
1514
|
-
* All three flags report EFFECT, not intent — each is the answer to "did this
|
|
1515
|
-
* operation change the line", measured across that operation alone. The count
|
|
1516
|
-
* and the ledger are claims about the FILE, so an edit that resolves to the
|
|
1517
|
-
* text already there has delivered nothing and must not be reported: retyping
|
|
1518
|
-
* an entity to the class it already is, or writing a positional slot the token
|
|
1519
|
-
* it already holds, used to count as a modification and reach the ledger as a
|
|
1520
|
-
* landed edit, over a byte-identical line. Discarded edits read the same way:
|
|
1521
|
-
* `applyAttributeMutations` drops a name its class has no slot for and
|
|
1522
|
-
* `retypeStepLine` returns an unparseable line untouched, and neither is a
|
|
1523
|
-
* modification of anything.
|
|
1524
|
-
*
|
|
1525
|
-
* `retyped` / `positional` matter most in a FULL export, which is where the
|
|
1526
|
-
* two are nominated (their edits have no earlier nomination site); named
|
|
1527
|
-
* attribute edits are nominated by the collection pass and `attributed` only
|
|
1528
|
-
* settles their delivery.
|
|
1529
|
-
*/
|
|
1530
|
-
applySourceLineMutations(expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive) {
|
|
1531
|
-
let text = entityText;
|
|
1532
|
-
let workingType = recordType.toUpperCase();
|
|
1533
|
-
const typeMutation = overlayActive && typeof this.mutationView.getEntityTypeMutation === 'function'
|
|
1534
|
-
? this.mutationView.getEntityTypeMutation(expressId)
|
|
1535
|
-
: null;
|
|
1536
|
-
let retyped = false;
|
|
1537
|
-
if (typeMutation) {
|
|
1538
|
-
const beforeRetype = text;
|
|
1539
|
-
text = retypeStepLine(text, recordType, typeMutation.newType, typeMutation.predefinedType ?? null, sourceSchema);
|
|
1540
|
-
retyped = text !== beforeRetype;
|
|
1541
|
-
// Set even for a no-op retype: the entity IS the target class from here
|
|
1542
|
-
// on, so the named and positional edits below must resolve against it.
|
|
1543
|
-
workingType = typeMutation.newType.toUpperCase();
|
|
1544
|
-
}
|
|
1545
|
-
// `applyAttributeMutations` returns its input UNCHANGED when it wrote
|
|
1546
|
-
// nothing — no slot resolved for any of the names, or the line does not
|
|
1547
|
-
// parse — so comparing is what tells the ledger whether a named attribute
|
|
1548
|
-
// edit was really carried, rather than merely attempted.
|
|
1549
|
-
let attributed = false;
|
|
1550
|
-
if (attributeMutations && attributeMutations.size > 0) {
|
|
1551
|
-
const beforeAttributes = text;
|
|
1552
|
-
text = this.applyAttributeMutations(text, workingType, attributeMutations);
|
|
1553
|
-
attributed = text !== beforeAttributes;
|
|
1554
|
-
}
|
|
1555
|
-
const positionals = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
1556
|
-
? this.mutationView.getPositionalMutationsForEntity(expressId)
|
|
1557
|
-
: null;
|
|
1558
|
-
let positional = false;
|
|
1559
|
-
if (positionals && positionals.size > 0) {
|
|
1560
|
-
const beforePositionals = text;
|
|
1561
|
-
text = this.applyPositionalMutations(text, positionals, workingType, sourceSchema);
|
|
1562
|
-
positional = text !== beforePositionals;
|
|
1563
|
-
}
|
|
1564
|
-
return { text, attributed, retyped, positional };
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Rewrite root IFC attributes directly on the original STEP entity line.
|
|
1568
|
-
*/
|
|
1569
|
-
applyAttributeMutations(entityText, entityType, attributeMutations) {
|
|
1570
|
-
const openParen = entityText.indexOf('(');
|
|
1571
|
-
const closeParen = entityText.lastIndexOf(');');
|
|
1572
|
-
if (openParen < 0 || closeParen < openParen) {
|
|
1573
|
-
return entityText;
|
|
1574
|
-
}
|
|
1575
|
-
// Cross-schema, not the IFC4 pin: an IFC4X3-only class (IfcCourse, IfcRoad,
|
|
1576
|
-
// IfcBridge, …) resolves no slots under the pin, so every named edit on one
|
|
1577
|
-
// was silently discarded here too. Identical for the 755 pinned classes
|
|
1578
|
-
// that declare attributes — `attribute-slot-types.test.ts` measures that —
|
|
1579
|
-
// so no IFC4 export changes; this only stops dropping edits it used to drop.
|
|
1580
|
-
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
1581
|
-
if (attrNames.length === 0) {
|
|
1582
|
-
return entityText;
|
|
1583
|
-
}
|
|
1584
|
-
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
1585
|
-
// A source line NEVER pads (unlike the overlay-created path): a short
|
|
1586
|
-
// argument list here means the file speaks a different schema, and growing
|
|
1587
|
-
// a record we did not author would corrupt it.
|
|
1588
|
-
let changed = false;
|
|
1589
|
-
for (const [attrName, value] of attributeMutations) {
|
|
1590
|
-
const index = attrNames.indexOf(attrName);
|
|
1591
|
-
if (index < 0 || index >= args.length)
|
|
1592
|
-
continue;
|
|
1593
|
-
// The source path shares every `$`-slot hole with the overlay-created
|
|
1594
|
-
// path, because a source record has plenty of `$` slots of its own. Both
|
|
1595
|
-
// go through the one helper below.
|
|
1596
|
-
args[index] = this.serializeNamedAttribute(entityType, index, value, args[index]);
|
|
1597
|
-
changed = true;
|
|
1598
|
-
}
|
|
1599
|
-
if (!changed) {
|
|
1600
|
-
return entityText;
|
|
1601
|
-
}
|
|
1602
|
-
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
|
-
* Serialize one NAMED attribute override into its slot — the single point
|
|
1606
|
-
* both the source-buffer rewrite and the overlay-created rewrite go through.
|
|
1607
|
-
*
|
|
1608
|
-
* `serializeAttributeValue` decides the STEP form by reading the token being
|
|
1609
|
-
* replaced, which is sound only while that token carries type information. A
|
|
1610
|
-
* `$` slot carries none, and both paths have plenty: a source record's
|
|
1611
|
-
* optional attributes are `$`, and overlay-created records pad missing slots
|
|
1612
|
-
* with `$`. So the declared type decides first, and inference is the fallback
|
|
1613
|
-
* for slots the schema does not classify (references, SELECTs, numerics),
|
|
1614
|
-
* where reading the old token is exactly the right heuristic.
|
|
1615
|
-
*/
|
|
1616
|
-
serializeNamedAttribute(entityType, index, value, currentToken) {
|
|
1617
|
-
if (getEnumTypedSlots(entityType).has(index))
|
|
1618
|
-
return serializeEnumToken(value);
|
|
1619
|
-
if (getStringTypedSlots(entityType).has(index))
|
|
1620
|
-
return serializeStringSlot(value);
|
|
1621
|
-
return serializeAttributeValue(value, currentToken);
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Apply overlay attribute + positional overrides to an OVERLAY-CREATED
|
|
1625
|
-
* entity's argument list (#2006).
|
|
1626
|
-
*
|
|
1627
|
-
* Distinct from {@link applyAttributeMutations} / {@link applyPositionalMutations},
|
|
1628
|
-
* which rewrite a line read out of the source buffer. Here the whole line is
|
|
1629
|
-
* ours: it was serialized moments ago from the creation payload, so the
|
|
1630
|
-
* argument list is the authoring payload's, not the file's. That difference
|
|
1631
|
-
* is why this PADS — `entity_create` takes whatever positional list the
|
|
1632
|
-
* caller passes, so a wall authored with three arguments still has a real
|
|
1633
|
-
* `Tag` slot at index 7, and dropping the edit because the payload was short
|
|
1634
|
-
* would be the very data loss this fixes. The source-buffer path must not
|
|
1635
|
-
* pad: there a short line means a different schema, and growing a record we
|
|
1636
|
-
* did not author would corrupt it.
|
|
1637
|
-
*
|
|
1638
|
-
* Named and positional overrides resolve to a slot index up front and share
|
|
1639
|
-
* ONE padding rule. Two padding rules on one record is how the next bug
|
|
1640
|
-
* starts, and the argument for padding — the class is fixed at creation time,
|
|
1641
|
-
* so a short payload is partial authoring — never depended on which of the
|
|
1642
|
-
* two APIs queued the edit.
|
|
1643
|
-
*/
|
|
1644
|
-
applyOverlayEntityOverrides(argsText, entityType, attributeOverrides, positionalOverrides, schemaVersion) {
|
|
1645
|
-
const args = argsText.length > 0 ? splitTopLevelArgs(argsText) : [];
|
|
1646
|
-
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
1647
|
-
const named = [];
|
|
1648
|
-
for (const [attrName, value] of attributeOverrides ?? []) {
|
|
1649
|
-
const index = attrNames.indexOf(attrName);
|
|
1650
|
-
if (index >= 0)
|
|
1651
|
-
named.push([index, value]);
|
|
1652
|
-
}
|
|
1653
|
-
// Grow to the class's FULL declared arity as soon as any override names a
|
|
1654
|
-
// declared slot the creation payload never reached. Growing only as far as
|
|
1655
|
-
// the edited slot would emit eight arguments for an IfcWall that declares
|
|
1656
|
-
// nine: this parser tolerates the truncated record, a schema-validating
|
|
1657
|
-
// consumer rejects the file.
|
|
1658
|
-
//
|
|
1659
|
-
// An index PAST the declared layout is not a slot at all, so it cannot
|
|
1660
|
-
// justify growing the record and stays dropped — as does any override on a
|
|
1661
|
-
// class neither schema source knows, where there is no arity to grow to.
|
|
1662
|
-
let needsPad = named.some(([index]) => index >= args.length);
|
|
1663
|
-
if (!needsPad && positionalOverrides) {
|
|
1664
|
-
for (const [index] of positionalOverrides) {
|
|
1665
|
-
if (index >= args.length && index < attrNames.length) {
|
|
1666
|
-
needsPad = true;
|
|
1667
|
-
break;
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
if (needsPad) {
|
|
1672
|
-
while (args.length < attrNames.length)
|
|
1673
|
-
args.push('$');
|
|
1674
|
-
}
|
|
1675
|
-
// Every `named` index is < attrNames.length by construction, and padding
|
|
1676
|
-
// has taken args.length to at least that, so each one lands.
|
|
1677
|
-
for (const [index, value] of named) {
|
|
1678
|
-
args[index] = this.serializeNamedAttribute(entityType, index, value, args[index]);
|
|
1679
|
-
}
|
|
1680
|
-
if (positionalOverrides && positionalOverrides.size > 0) {
|
|
1681
|
-
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1682
|
-
for (const [index, value] of positionalOverrides) {
|
|
1683
|
-
if (index < 0 || index >= args.length)
|
|
1684
|
-
continue;
|
|
1685
|
-
args[index] = this.serializePositionalOverride(entityType, index, value, args[index], realSlots, schemaVersion);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return args.join(',');
|
|
1689
|
-
}
|
|
1690
|
-
/**
|
|
1691
|
-
* Apply positional STEP argument overrides to an entity line.
|
|
1692
|
-
* Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
|
|
1693
|
-
* have no symbolic names. Indexes that fall outside the existing arg list
|
|
1694
|
-
* are silently ignored.
|
|
1695
|
-
*/
|
|
1696
|
-
applyPositionalMutations(entityText, positionals, entityType, schemaVersion) {
|
|
1697
|
-
const openParen = entityText.indexOf('(');
|
|
1698
|
-
const closeParen = entityText.lastIndexOf(');');
|
|
1699
|
-
if (openParen < 0 || closeParen < openParen)
|
|
1700
|
-
return entityText;
|
|
1701
|
-
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
1702
|
-
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1703
|
-
let changed = false;
|
|
1704
|
-
for (const [index, value] of positionals) {
|
|
1705
|
-
if (index < 0 || index >= args.length)
|
|
1706
|
-
continue;
|
|
1707
|
-
args[index] = this.serializePositionalOverride(entityType, index, value, args[index], realSlots, schemaVersion);
|
|
1708
|
-
changed = true;
|
|
1709
|
-
}
|
|
1710
|
-
if (!changed)
|
|
1711
|
-
return entityText;
|
|
1712
|
-
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
1713
|
-
}
|
|
1714
|
-
/**
|
|
1715
|
-
* Serialize one positional override, composing the schema-aware passes:
|
|
1716
|
-
* explicit `{ real }`/`{ typed }` marker → SELECT auto-qualification
|
|
1717
|
-
* (`IFCBOOLEAN(.T.)`) → REAL forcing. For REAL forcing the current source
|
|
1718
|
-
* token is a secondary signal: replacing a value that was already a REAL
|
|
1719
|
-
* (`0.4`, `1.5E-7`) keeps it REAL even for entities the XSD index doesn't
|
|
1720
|
-
* cover, so a whole-number edit can't silently downgrade the slot.
|
|
1721
|
-
*/
|
|
1722
|
-
serializePositionalOverride(entityType, index, value, currentToken, realSlots, schemaVersion) {
|
|
1723
|
-
if (isTypedMarker(value))
|
|
1724
|
-
return serializeStepValue(value);
|
|
1725
|
-
const qualified = serializeQualifiedSelectSlot(entityType, index, value);
|
|
1726
|
-
if (qualified !== null)
|
|
1727
|
-
return qualified;
|
|
1728
|
-
const forceReal = realSlots.has(index) || tokenIsRealLiteral(currentToken);
|
|
1729
|
-
return serializeStepValue(value, forceReal);
|
|
1730
|
-
}
|
|
1731
|
-
resolveMapUnitReference(unitName, newGeorefLines, effective) {
|
|
1732
|
-
const normalized = this.normalizeMapUnitName(unitName);
|
|
1733
|
-
const existing = this.findLengthUnitReference(normalized, effective);
|
|
1734
|
-
if (existing !== null) {
|
|
1735
|
-
return existing;
|
|
1736
|
-
}
|
|
1737
|
-
if (normalized === 'METRE') {
|
|
1738
|
-
const unitId = this.nextExpressId++;
|
|
1739
|
-
newGeorefLines.push(`#${unitId}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);`);
|
|
1740
|
-
return unitId;
|
|
1741
|
-
}
|
|
1742
|
-
if (normalized === 'FOOT' || normalized === 'US SURVEY FOOT') {
|
|
1743
|
-
const dimId = this.nextExpressId++;
|
|
1744
|
-
const siUnitId = this.nextExpressId++;
|
|
1745
|
-
const measureId = this.nextExpressId++;
|
|
1746
|
-
const convUnitId = this.nextExpressId++;
|
|
1747
|
-
const factor = normalized === 'US SURVEY FOOT' ? 1200 / 3937 : 0.3048;
|
|
1748
|
-
const name = normalized === 'US SURVEY FOOT' ? 'US SURVEY FOOT' : 'FOOT';
|
|
1749
|
-
newGeorefLines.push(`#${dimId}=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);`);
|
|
1750
|
-
newGeorefLines.push(`#${siUnitId}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);`);
|
|
1751
|
-
newGeorefLines.push(`#${measureId}=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(${toStepReal(factor)}),#${siUnitId});`);
|
|
1752
|
-
newGeorefLines.push(`#${convUnitId}=IFCCONVERSIONBASEDUNIT(#${dimId},.LENGTHUNIT.,'${name}',#${measureId});`);
|
|
1753
|
-
return convUnitId;
|
|
1754
|
-
}
|
|
1755
|
-
const fallbackId = this.nextExpressId++;
|
|
1756
|
-
newGeorefLines.push(`#${fallbackId}=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);`);
|
|
1757
|
-
return fallbackId;
|
|
1758
|
-
}
|
|
1759
|
-
normalizeMapUnitName(unitName) {
|
|
1760
|
-
const normalized = unitName.trim().toUpperCase().replace(/\s+/g, ' ');
|
|
1761
|
-
if (normalized.includes('US SURVEY FOOT'))
|
|
1762
|
-
return 'US SURVEY FOOT';
|
|
1763
|
-
if (normalized.includes('METER') || normalized.includes('METRE'))
|
|
1764
|
-
return 'METRE';
|
|
1765
|
-
if (normalized.includes('FOOT') || normalized.includes('FEET'))
|
|
1766
|
-
return 'FOOT';
|
|
1767
|
-
return normalized;
|
|
1768
|
-
}
|
|
1769
|
-
/**
|
|
1770
|
-
* `effective` filters the candidates the same way the georef reads above do:
|
|
1771
|
-
* returning a tombstoned unit id hands the caller a `#id` for a line the
|
|
1772
|
-
* export never writes. Returning null instead makes `resolveMapUnitReference`
|
|
1773
|
-
* synthesise a fresh unit, which is the outcome a deleted unit deserves.
|
|
1774
|
-
*/
|
|
1775
|
-
findLengthUnitReference(preferredUnitName, effective) {
|
|
1776
|
-
if (!this.entityExtractor)
|
|
1777
|
-
return null;
|
|
1778
|
-
// Only source records carry the bytes `extractEntity` reads, so an
|
|
1779
|
-
// overlay-created project is skipped rather than shadowing the file's own.
|
|
1780
|
-
const projectId = (effective.byType.get('IFCPROJECT') ?? []).find((id) => this.dataStore.entityIndex.byId.has(id));
|
|
1781
|
-
const projectRef = projectId !== undefined ? this.dataStore.entityIndex.byId.get(projectId) : undefined;
|
|
1782
|
-
const project = projectRef ? this.entityExtractor.extractEntity(projectRef) : null;
|
|
1783
|
-
const unitAssignmentId = project?.attributes?.[8];
|
|
1784
|
-
if (typeof unitAssignmentId !== 'number' || effective.isDeleted(unitAssignmentId))
|
|
1785
|
-
return null;
|
|
1786
|
-
const unitAssignmentRef = this.dataStore.entityIndex.byId.get(unitAssignmentId);
|
|
1787
|
-
const unitAssignment = unitAssignmentRef ? this.entityExtractor.extractEntity(unitAssignmentRef) : null;
|
|
1788
|
-
const units = unitAssignment?.attributes?.[0];
|
|
1789
|
-
if (!Array.isArray(units))
|
|
1790
|
-
return null;
|
|
1791
|
-
for (const unitId of units) {
|
|
1792
|
-
if (typeof unitId !== 'number' || effective.isDeleted(unitId))
|
|
1793
|
-
continue;
|
|
1794
|
-
const unitRef = this.dataStore.entityIndex.byId.get(unitId);
|
|
1795
|
-
const unit = unitRef ? this.entityExtractor.extractEntity(unitRef) : null;
|
|
1796
|
-
if (!unit)
|
|
1797
|
-
continue;
|
|
1798
|
-
const typeName = unit.type.toUpperCase();
|
|
1799
|
-
const attrs = unit.attributes ?? [];
|
|
1800
|
-
const unitType = typeof attrs[1] === 'string' ? attrs[1].replace(/\./g, '').toUpperCase() : '';
|
|
1801
|
-
if (unitType !== 'LENGTHUNIT')
|
|
1802
|
-
continue;
|
|
1803
|
-
if (typeName === 'IFCSIUNIT') {
|
|
1804
|
-
const prefix = typeof attrs[2] === 'string' ? attrs[2].replace(/\./g, '').toUpperCase() : '';
|
|
1805
|
-
const name = typeof attrs[3] === 'string' ? attrs[3].replace(/\./g, '').toUpperCase() : '';
|
|
1806
|
-
const combined = prefix ? `${prefix}${name}` : name;
|
|
1807
|
-
if (preferredUnitName === 'METRE' && (combined === 'METRE' || combined === 'METER')) {
|
|
1808
|
-
return unitId;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
if (typeName === 'IFCCONVERSIONBASEDUNIT') {
|
|
1812
|
-
const name = typeof attrs[2] === 'string' ? this.normalizeMapUnitName(attrs[2]) : '';
|
|
1813
|
-
if (name === preferredUnitName) {
|
|
1814
|
-
return unitId;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
return null;
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Record that a requested IfcMapConversion could not be written. Emitting it
|
|
1822
|
-
* anyway would leave `SourceCRS` pointing at nothing, so the refusal is the
|
|
1823
|
-
* correct output — but the file alone cannot express it, which is why it goes
|
|
1824
|
-
* back to the caller in `stats.warnings` as well as to the console (#2067).
|
|
1825
|
-
*/
|
|
1826
|
-
reportMapConversionRefused(warnings) {
|
|
1827
|
-
warnings.push(MAP_CONVERSION_WITHOUT_CONTEXT_WARNING);
|
|
1828
|
-
console.warn(`[StepExporter] ${MAP_CONVERSION_WITHOUT_CONTEXT_WARNING}`);
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* Record that a requested IfcMapConversion could not be written because
|
|
1832
|
-
* there is no IfcProjectedCRS to attach it to — a different refusal from
|
|
1833
|
-
* {@link reportMapConversionRefused}: "no CRS to attach it to" rather than
|
|
1834
|
-
* "no context to reference" (#2105).
|
|
1835
|
-
*/
|
|
1836
|
-
reportMapConversionRefusedNoCrs(warnings) {
|
|
1837
|
-
warnings.push(MAP_CONVERSION_WITHOUT_CRS_WARNING);
|
|
1838
|
-
console.warn(`[StepExporter] ${MAP_CONVERSION_WITHOUT_CRS_WARNING}`);
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* `effective` again: the id returned here becomes the new IfcMapConversion's
|
|
1842
|
-
* SourceCRS, so a tombstoned context would leave the created line pointing at
|
|
1843
|
-
* a record the export skips — a dangling reference and an invalid file.
|
|
1844
|
-
*/
|
|
1845
|
-
findPreferredGeometricRepresentationContextId(effective) {
|
|
1846
|
-
if (!this.entityExtractor)
|
|
1847
|
-
return null;
|
|
1848
|
-
const contextIds = (effective.byType.get('IFCGEOMETRICREPRESENTATIONCONTEXT') ?? [])
|
|
1849
|
-
.filter((id) => this.dataStore.entityIndex.byId.has(id));
|
|
1850
|
-
let first3dContext = null;
|
|
1851
|
-
for (const contextId of contextIds) {
|
|
1852
|
-
const contextRef = this.dataStore.entityIndex.byId.get(contextId);
|
|
1853
|
-
const context = contextRef ? this.entityExtractor.extractEntity(contextRef) : null;
|
|
1854
|
-
if (!context)
|
|
1855
|
-
continue;
|
|
1856
|
-
const attrs = context.attributes ?? [];
|
|
1857
|
-
const contextType = typeof attrs[1] === 'string' ? attrs[1].trim().toUpperCase() : '';
|
|
1858
|
-
const dimension = typeof attrs[2] === 'number' ? attrs[2] : null;
|
|
1859
|
-
if (dimension === 3 && first3dContext === null) {
|
|
1860
|
-
first3dContext = contextId;
|
|
1861
|
-
}
|
|
1862
|
-
if (contextType === 'MODEL' && dimension === 3) {
|
|
1863
|
-
return contextId;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
return first3dContext ?? contextIds[0] ?? null;
|
|
1867
|
-
}
|
|
1868
|
-
/**
|
|
1869
|
-
* Generate a new IFC GlobalId (22 character base64). `random` is the
|
|
1870
|
-
* export's optional seeded source (`StepExportOptions.guidRandom`);
|
|
1871
|
-
* undefined keeps the default random path.
|
|
1872
|
-
*/
|
|
1873
|
-
generateGlobalId(random) {
|
|
1874
|
-
return generateIfcGuid(random);
|
|
1875
|
-
}
|
|
1876
195
|
/**
|
|
1877
196
|
* Find the maximum EXPRESS ID in the data store
|
|
1878
197
|
*/
|
|
@@ -1882,273 +201,66 @@ export class StepExporter {
|
|
|
1882
201
|
return getMaxExpressId(getCompleteEntityIndex(this.dataStore));
|
|
1883
202
|
}
|
|
1884
203
|
/**
|
|
1885
|
-
*
|
|
1886
|
-
*/
|
|
1887
|
-
findUnitId(unitName, effective) {
|
|
1888
|
-
return this.findLengthUnitReference(this.normalizeMapUnitName(unitName), effective);
|
|
1889
|
-
}
|
|
1890
|
-
/**
|
|
1891
|
-
* Check if an entity type is a geometry-related type
|
|
1892
|
-
*/
|
|
1893
|
-
isGeometryEntity(type) {
|
|
1894
|
-
const geometryTypes = new Set([
|
|
1895
|
-
'IFCCARTESIANPOINT',
|
|
1896
|
-
'IFCDIRECTION',
|
|
1897
|
-
'IFCAXIS2PLACEMENT2D',
|
|
1898
|
-
'IFCAXIS2PLACEMENT3D',
|
|
1899
|
-
'IFCLOCALPLACEMENT',
|
|
1900
|
-
'IFCSHAPEREPRESENTATION',
|
|
1901
|
-
'IFCPRODUCTDEFINITIONSHAPE',
|
|
1902
|
-
'IFCGEOMETRICREPRESENTATIONCONTEXT',
|
|
1903
|
-
'IFCGEOMETRICREPRESENTATIONSUBCONTEXT',
|
|
1904
|
-
'IFCEXTRUDEDAREASOLID',
|
|
1905
|
-
'IFCFACETEDBREP',
|
|
1906
|
-
'IFCPOLYLOOP',
|
|
1907
|
-
'IFCFACE',
|
|
1908
|
-
'IFCFACEOUTERBOUND',
|
|
1909
|
-
'IFCCLOSEDSHELL',
|
|
1910
|
-
'IFCRECTANGLEPROFILEDEF',
|
|
1911
|
-
'IFCCIRCLEPROFILEDEF',
|
|
1912
|
-
'IFCARBITRARYCLOSEDPROFILEDEF',
|
|
1913
|
-
'IFCPOLYLINE',
|
|
1914
|
-
'IFCTRIMMEDCURVE',
|
|
1915
|
-
'IFCBSPLINECURVE',
|
|
1916
|
-
'IFCBSPLINESURFACE',
|
|
1917
|
-
'IFCTRIANGULATEDFACESET',
|
|
1918
|
-
'IFCPOLYGONALFACE',
|
|
1919
|
-
'IFCINDEXEDPOLYGONALFACE',
|
|
1920
|
-
'IFCPOLYGONALFACESET',
|
|
1921
|
-
'IFCSTYLEDITEM',
|
|
1922
|
-
'IFCPRESENTATIONSTYLEASSIGNMENT',
|
|
1923
|
-
'IFCSURFACESTYLE',
|
|
1924
|
-
'IFCSURFACESTYLERENDERING',
|
|
1925
|
-
'IFCCOLOURRGB',
|
|
1926
|
-
]);
|
|
1927
|
-
return geometryTypes.has(type);
|
|
1928
|
-
}
|
|
1929
|
-
/**
|
|
1930
|
-
* Build a one-shot reverse index of every IfcRelDefinesByProperties in
|
|
1931
|
-
* the source: for each related entity, list the rels and property/quantity
|
|
1932
|
-
* sets that reference it. Used by the export pre-pass so the per-entity
|
|
1933
|
-
* "find owning rels" step is O(K) rather than O(N) per modified entity.
|
|
204
|
+
* The exporter state `step-georeferencing.ts` cannot read off the pass.
|
|
1934
205
|
*
|
|
1935
|
-
* `
|
|
1936
|
-
*
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
for (const entityId of related) {
|
|
1950
|
-
let bucket = byEntity.get(entityId);
|
|
1951
|
-
if (!bucket) {
|
|
1952
|
-
bucket = [];
|
|
1953
|
-
byEntity.set(entityId, bucket);
|
|
1954
|
-
}
|
|
1955
|
-
bucket.push({ relId, psetId });
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
return { byEntity, relatedByRel };
|
|
206
|
+
* `allocateExpressId` hands out ids from THIS exporter's `nextExpressId`,
|
|
207
|
+
* which the property-set and quantity-set generators in
|
|
208
|
+
* `step-property-sets.ts` increment at six further sites through the same
|
|
209
|
+
* callback — hoisting the counter onto the pass would change what it
|
|
210
|
+
* computes, not merely where it is named, so both phases get a callback
|
|
211
|
+
* instead (#2475 step 2a).
|
|
212
|
+
*/
|
|
213
|
+
georefContext(deltaOnly) {
|
|
214
|
+
return {
|
|
215
|
+
dataStore: this.dataStore,
|
|
216
|
+
entityExtractor: this.entityExtractor,
|
|
217
|
+
allocateExpressId: () => this.nextExpressId++,
|
|
218
|
+
deltaOnly,
|
|
219
|
+
};
|
|
1959
220
|
}
|
|
1960
221
|
/**
|
|
1961
|
-
* The
|
|
1962
|
-
* to read.
|
|
1963
|
-
*
|
|
1964
|
-
* The byte check is on the RANGE, not on `dataStore.source`. `source` is a
|
|
1965
|
-
* MANDATORY accessor — `EMPTY_SOURCE_BYTES` is how "this model kept no bytes"
|
|
1966
|
-
* is spelled (server-parsed, synthetic, GLB and point-cloud stores all have
|
|
1967
|
-
* one) — so the `!this.dataStore.source` guard the five readers below used to
|
|
1968
|
-
* carry never fired. It was also redundant: a zero-length range decodes to
|
|
1969
|
-
* `''`, which fails every regex those readers run, so they already answered
|
|
1970
|
-
* "nothing" for a sourceless store. Scoping the check to the range is what
|
|
1971
|
-
* makes the guard live without changing a single answer, the same shape and
|
|
1972
|
-
* for the same reason as `reference-collector.ts` (#2339).
|
|
1973
|
-
*
|
|
1974
|
-
* An OVERLAY-created entity never reaches here: every caller resolves its id
|
|
1975
|
-
* through `dataStore.entityIndex.byId`, which holds source records only
|
|
1976
|
-
* (`effective-index.ts` synthesises the overlay refs on its own side and
|
|
1977
|
-
* writes nothing back), so an overlay id is already `undefined` at the
|
|
1978
|
-
* lookup and is served by the callers' documented "not a source record"
|
|
1979
|
-
* path. That is why an early return is safe HERE and is NOT safe at the
|
|
1980
|
-
* visible-only closure in `export` — see the comment there.
|
|
222
|
+
* The state `step-property-sets.ts` cannot read off the pass (#2475 2b/2c).
|
|
1981
223
|
*
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
1990
|
-
* two files is an argument that has to stay true in two files.
|
|
1991
|
-
*
|
|
1992
|
-
* The consequence specific to THIS site is that the wrong answer is acted
|
|
1993
|
-
* on. `retainSharedAtoms` un-skips every id `getPropertyIdsInSet` returns,
|
|
1994
|
-
* so a member list read out of the wrong record un-skips the wrong atoms;
|
|
1995
|
-
* and the source-iteration pass already refuses to emit a record whose ref
|
|
1996
|
-
* fails `isReadableSourceRef` (see the `continue` in `export`), so before
|
|
1997
|
-
* this gate these readers were making decisions on behalf of a container
|
|
1998
|
-
* that the same export had decided not to write. Gating them on the same
|
|
1999
|
-
* predicate is what makes the two passes agree.
|
|
2000
|
-
*
|
|
2001
|
-
* The degradation is the one the exporter already handles: a record with no
|
|
2002
|
-
* emittable bytes, generating nothing and named by nothing. It costs one
|
|
2003
|
-
* answer that used to be right by luck — an overrunning ref on the file's
|
|
2004
|
-
* LAST record clamps back to exactly that record's text — but that record
|
|
2005
|
-
* is one the emission pass drops anyway, so keeping the answer only kept
|
|
2006
|
-
* the disagreement.
|
|
2007
|
-
*/
|
|
2008
|
-
entityLineText(entityId) {
|
|
2009
|
-
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
2010
|
-
if (!entityRef || !this.isReadableSourceRef(entityRef))
|
|
2011
|
-
return null;
|
|
2012
|
-
return decodeRange(this.dataStore.source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
2013
|
-
}
|
|
2014
|
-
/**
|
|
2015
|
-
* Get entity IDs related by IfcRelDefinesByProperties (the related objects)
|
|
2016
|
-
*/
|
|
2017
|
-
getRelatedEntities(relId) {
|
|
2018
|
-
const entityText = this.entityLineText(relId);
|
|
2019
|
-
if (entityText === null)
|
|
2020
|
-
return [];
|
|
2021
|
-
// Parse IfcRelDefinesByProperties: #ID=IFCRELDEFINESBYPROPERTIES('guid',$,$,$,(#objects),#pset);
|
|
2022
|
-
// The 5th argument (index 4) is the list of related objects
|
|
2023
|
-
const match = entityText.match(/\(([^)]+)\)\s*,\s*#(\d+)\s*\)\s*;/);
|
|
2024
|
-
if (!match)
|
|
2025
|
-
return [];
|
|
2026
|
-
const objectsList = match[1];
|
|
2027
|
-
const refs = [];
|
|
2028
|
-
const refMatches = objectsList.matchAll(/#(\d+)/g);
|
|
2029
|
-
for (const m of refMatches) {
|
|
2030
|
-
refs.push(parseInt(m[1], 10));
|
|
2031
|
-
}
|
|
2032
|
-
return refs;
|
|
2033
|
-
}
|
|
2034
|
-
/**
|
|
2035
|
-
* Get the property set ID from IfcRelDefinesByProperties
|
|
2036
|
-
*/
|
|
2037
|
-
getRelatedPropertySet(relId) {
|
|
2038
|
-
const entityText = this.entityLineText(relId);
|
|
2039
|
-
if (entityText === null)
|
|
2040
|
-
return null;
|
|
2041
|
-
// Last #ID before the closing );
|
|
2042
|
-
const match = entityText.match(/,\s*#(\d+)\s*\)\s*;$/);
|
|
2043
|
-
if (!match)
|
|
2044
|
-
return null;
|
|
2045
|
-
return parseInt(match[1], 10);
|
|
2046
|
-
}
|
|
2047
|
-
/**
|
|
2048
|
-
* Get the name of a property set by parsing the entity
|
|
2049
|
-
*/
|
|
2050
|
-
getPropertySetName(psetId) {
|
|
2051
|
-
const entityText = this.entityLineText(psetId);
|
|
2052
|
-
if (entityText === null)
|
|
2053
|
-
return null;
|
|
2054
|
-
// Parse: IFCPROPERTYSET('guid',$,'Name',$,...) - Name is 3rd argument
|
|
2055
|
-
const match = entityText.match(/IFCPROPERTYSET\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
2056
|
-
if (!match)
|
|
2057
|
-
return null;
|
|
2058
|
-
return match[1];
|
|
2059
|
-
}
|
|
2060
|
-
/**
|
|
2061
|
-
* Get the name of an element quantity set by parsing the entity
|
|
2062
|
-
*/
|
|
2063
|
-
getElementQuantityName(entityId) {
|
|
2064
|
-
const entityText = this.entityLineText(entityId);
|
|
2065
|
-
if (entityText === null)
|
|
2066
|
-
return null;
|
|
2067
|
-
// Parse: IFCELEMENTQUANTITY('guid',$,'Name',...) - Name is 3rd argument
|
|
2068
|
-
const match = entityText.match(/IFCELEMENTQUANTITY\s*\([^,]*,[^,]*,'([^']*)'/i);
|
|
2069
|
-
if (!match)
|
|
2070
|
-
return null;
|
|
2071
|
-
return match[1];
|
|
2072
|
-
}
|
|
2073
|
-
/**
|
|
2074
|
-
* Get IDs of properties in a property set
|
|
2075
|
-
*/
|
|
2076
|
-
/**
|
|
2077
|
-
* Un-skip property/quantity atoms that a surviving (non-skipped, and — under
|
|
2078
|
-
* visible-only export — still-included) IfcPropertySet / IfcElementQuantity
|
|
2079
|
-
* still references.
|
|
224
|
+
* `allocateExpressId` is the same callback `georefContext` hands out, over
|
|
225
|
+
* the same counter, so the ids the two phases allocate stay in one sequence.
|
|
226
|
+
* `ownerHistory` is passed by reference — the object is this exporter's, and
|
|
227
|
+
* `export()` resets it. `isReadableSourceRef` is the instance predicate, not
|
|
228
|
+
* `pass.isReadableSourceRef`, because two consumers of that module
|
|
229
|
+
* (`buildRelDefinesByPropertiesIndex`, and `retainSharedAtoms` in
|
|
230
|
+
* `step-source-iteration.ts`) run with no pass in hand; both readers are
|
|
231
|
+
* built over the same source.
|
|
2080
232
|
*
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
* This pass restores any such atom: the edited pset still emits its replacement
|
|
2086
|
-
* with the new value, while the shared atom stays for the psets that keep their
|
|
2087
|
-
* original value.
|
|
233
|
+
* Rebuilt per call, as `georefContext` is: every call site runs once per
|
|
234
|
+
* export bar `retainSharedAtoms`, which hoists it out of its loop — hence
|
|
235
|
+
* `buildSourceIterationContext` (`step-export-contexts.ts`) takes this as a
|
|
236
|
+
* thunk rather than a value.
|
|
2088
237
|
*/
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
if (skipIds.has(containerId))
|
|
2100
|
-
continue;
|
|
2101
|
-
// Under visible-only export a container outside the closure is not emitted,
|
|
2102
|
-
// so it cannot keep an atom alive.
|
|
2103
|
-
if (allowedEntityIds !== null && !allowedEntityIds.has(containerId))
|
|
2104
|
-
continue;
|
|
2105
|
-
for (const atomId of this.getPropertyIdsInSet(containerId)) {
|
|
2106
|
-
skipIds.delete(atomId);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
getPropertyIdsInSet(psetId) {
|
|
2111
|
-
const entityText = this.entityLineText(psetId);
|
|
2112
|
-
if (entityText === null)
|
|
2113
|
-
return [];
|
|
2114
|
-
// Parse: IFCPROPERTYSET(...,(#prop1,#prop2,...)); - Last argument is properties list
|
|
2115
|
-
const match = entityText.match(/\(\s*(#[^)]+)\s*\)\s*\)\s*;$/);
|
|
2116
|
-
if (!match)
|
|
2117
|
-
return [];
|
|
2118
|
-
const propsList = match[1];
|
|
2119
|
-
const ids = [];
|
|
2120
|
-
const refMatches = propsList.matchAll(/#(\d+)/g);
|
|
2121
|
-
for (const m of refMatches) {
|
|
2122
|
-
ids.push(parseInt(m[1], 10));
|
|
2123
|
-
}
|
|
2124
|
-
return ids;
|
|
238
|
+
propertySetContext() {
|
|
239
|
+
return {
|
|
240
|
+
dataStore: this.dataStore,
|
|
241
|
+
entityExtractor: this.entityExtractor,
|
|
242
|
+
mutationView: this.mutationView,
|
|
243
|
+
isReadableSourceRef: this.isReadableSourceRef,
|
|
244
|
+
allocateExpressId: () => this.nextExpressId++,
|
|
245
|
+
ownerHistory: this.ownerHistory,
|
|
246
|
+
applySourceLineMutations: (expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected) => applySourceLineMutations(this.mutationView, expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected),
|
|
247
|
+
};
|
|
2125
248
|
}
|
|
2126
249
|
/**
|
|
2127
|
-
* The
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
* string form. Reading only the source made every pset on a created
|
|
2134
|
-
* `IfcWallType` look unowned, which is how it ended up on an occurrence
|
|
2135
|
-
* relation instead (#2012).
|
|
250
|
+
* The state `step-collection.ts` cannot read off the pass (#2475, the
|
|
251
|
+
* collection block). `propertySetContext` and `georefContext` are handed
|
|
252
|
+
* over as the SAME thunks {@link propertySetContext} and
|
|
253
|
+
* {@link georefContext} already are — this phase calls the first twice per
|
|
254
|
+
* export and the second once, and nothing here should change how often
|
|
255
|
+
* either is rebuilt.
|
|
2136
256
|
*/
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
const entityRef = this.dataStore.entityIndex.byId.get(entityId);
|
|
2145
|
-
if (!entityRef)
|
|
2146
|
-
return [];
|
|
2147
|
-
const entity = this.entityExtractor.extractEntity(entityRef);
|
|
2148
|
-
const hasPropertySets = entity?.attributes?.[HAS_PROPERTY_SETS_SLOT];
|
|
2149
|
-
if (!Array.isArray(hasPropertySets))
|
|
2150
|
-
return [];
|
|
2151
|
-
return hasPropertySets.filter((value) => typeof value === 'number');
|
|
257
|
+
collectionContext() {
|
|
258
|
+
return {
|
|
259
|
+
dataStore: this.dataStore,
|
|
260
|
+
mutationView: this.mutationView,
|
|
261
|
+
propertySetContext: () => this.propertySetContext(),
|
|
262
|
+
georefContext: (deltaOnly) => this.georefContext(deltaOnly),
|
|
263
|
+
};
|
|
2152
264
|
}
|
|
2153
265
|
}
|
|
2154
266
|
/**
|