@ifc-lite/export 2.9.4 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/declared-property-type.d.ts +19 -8
- package/dist/declared-property-type.d.ts.map +1 -1
- package/dist/declared-property-type.js +31 -9
- package/dist/declared-property-type.js.map +1 -1
- 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 +19 -0
- package/dist/merged-exporter.d.ts.map +1 -1
- package/dist/merged-exporter.js +23 -41
- package/dist/merged-exporter.js.map +1 -1
- package/dist/parquet-exporter.d.ts +1 -3
- package/dist/parquet-exporter.d.ts.map +1 -1
- package/dist/parquet-exporter.js +1 -0
- package/dist/parquet-exporter.js.map +1 -1
- package/dist/property-value-serialization.d.ts +54 -0
- package/dist/property-value-serialization.d.ts.map +1 -0
- package/dist/property-value-serialization.js +134 -0
- package/dist/property-value-serialization.js.map +1 -0
- package/dist/reference-collector.d.ts +20 -0
- package/dist/reference-collector.d.ts.map +1 -1
- package/dist/reference-collector.js +46 -113
- package/dist/reference-collector.js.map +1 -1
- package/dist/schema-converter.d.ts.map +1 -1
- package/dist/schema-converter.js +46 -96
- package/dist/schema-converter.js.map +1 -1
- package/dist/source-ref-bounds.d.ts +3 -3
- package/dist/source-ref-bounds.js +3 -3
- package/dist/step-attribute-mutations.d.ts +49 -0
- package/dist/step-attribute-mutations.d.ts.map +1 -0
- package/dist/step-attribute-mutations.js +172 -0
- package/dist/step-attribute-mutations.js.map +1 -0
- package/dist/step-attribute-serializers.d.ts +41 -0
- package/dist/step-attribute-serializers.d.ts.map +1 -0
- package/dist/step-attribute-serializers.js +74 -0
- package/dist/step-attribute-serializers.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 +126 -0
- package/dist/step-collection.js.map +1 -0
- package/dist/step-export-contexts.d.ts +61 -0
- package/dist/step-export-contexts.d.ts.map +1 -0
- package/dist/step-export-contexts.js +50 -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 +23 -354
- package/dist/step-exporter.d.ts.map +1 -1
- package/dist/step-exporter.js +76 -1260
- package/dist/step-exporter.js.map +1 -1
- package/dist/step-geometry-types.d.ts +3 -0
- package/dist/step-geometry-types.d.ts.map +1 -0
- package/dist/step-geometry-types.js +73 -0
- package/dist/step-geometry-types.js.map +1 -0
- package/dist/step-georeferencing.d.ts +0 -10
- package/dist/step-georeferencing.d.ts.map +1 -1
- package/dist/step-georeferencing.js +46 -94
- package/dist/step-georeferencing.js.map +1 -1
- package/dist/step-header.d.ts +53 -0
- package/dist/step-header.d.ts.map +1 -0
- package/dist/step-header.js +63 -0
- package/dist/step-header.js.map +1 -0
- package/dist/step-map-unit.d.ts +90 -0
- package/dist/step-map-unit.d.ts.map +1 -0
- package/dist/step-map-unit.js +277 -0
- package/dist/step-map-unit.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-attribute-overrides.d.ts +32 -0
- package/dist/step-overlay-attribute-overrides.d.ts.map +1 -0
- package/dist/step-overlay-attribute-overrides.js +83 -0
- package/dist/step-overlay-attribute-overrides.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 +45 -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-set-collection.d.ts +24 -0
- package/dist/step-property-set-collection.d.ts.map +1 -0
- package/dist/step-property-set-collection.js +286 -0
- package/dist/step-property-set-collection.js.map +1 -0
- package/dist/step-property-set-generators.d.ts +20 -0
- package/dist/step-property-set-generators.d.ts.map +1 -0
- package/dist/step-property-set-generators.js +98 -0
- package/dist/step-property-set-generators.js.map +1 -0
- package/dist/step-property-set-index.d.ts +25 -0
- package/dist/step-property-set-index.d.ts.map +1 -0
- package/dist/step-property-set-index.js +69 -0
- package/dist/step-property-set-index.js.map +1 -0
- package/dist/step-property-set-readers.d.ts +158 -0
- package/dist/step-property-set-readers.d.ts.map +1 -0
- package/dist/step-property-set-readers.js +251 -0
- package/dist/step-property-set-readers.js.map +1 -0
- package/dist/step-property-sets.d.ts +2 -184
- package/dist/step-property-sets.d.ts.map +1 -1
- package/dist/step-property-sets.js +22 -674
- package/dist/step-property-sets.js.map +1 -1
- package/dist/step-serialization.d.ts +33 -52
- package/dist/step-serialization.d.ts.map +1 -1
- package/dist/step-serialization.js +69 -131
- package/dist/step-serialization.js.map +1 -1
- package/dist/step-source-iteration.d.ts +97 -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,42 +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 { EntityExtractor,
|
|
5
|
-
import {
|
|
6
|
-
import { convertStepLine, needsConversion } from './schema-converter.js';
|
|
7
|
-
import { retypeStepLine, retypeArgTokens } from './retype.js';
|
|
4
|
+
import { EntityExtractor, parseSourceHeader } from '@ifc-lite/parser';
|
|
5
|
+
import { needsConversion } from './schema-converter.js';
|
|
8
6
|
import { getCompleteEntityIndex, getMaxExpressId } from './entity-iteration.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { getEnumTypedSlots, getStringTypedSlots, serializeEnumToken, serializeStringSlot, } from './attribute-slot-types.js';
|
|
21
|
-
import { serializeQualifiedSelectSlot } from './select-qualification.js';
|
|
22
|
-
/**
|
|
23
|
-
* Message for a relationship this export DROPPED rather than rewrote.
|
|
24
|
-
*
|
|
25
|
-
* `filterHiddenRefsFromRelationshipLine` removes an omitted `#N` from a
|
|
26
|
-
* SET/LIST attribute, but a single-valued attribute has no STEP spelling for
|
|
27
|
-
* "omitted" and an empty SET is not the same statement as the original — so in
|
|
28
|
-
* both of those cases it withholds the whole line and the relationship simply
|
|
29
|
-
* is not in the output. Withholding beats shipping a dangling `#N`, but it is
|
|
30
|
-
* not free: every OTHER entity that relationship named loses the association.
|
|
31
|
-
* A visible element can therefore come out of a plain full export with one
|
|
32
|
-
* fewer pset than it went in with, and before this warning existed nothing in
|
|
33
|
-
* the result said so (adversarial review of #2668).
|
|
34
|
-
*
|
|
35
|
-
* Deliberately reports the relationship rather than the omitted target: the
|
|
36
|
-
* target's own omission is already the caller's own doing in every reason but
|
|
37
|
-
* the unreadable-ref one, whereas the lost association is the surprise.
|
|
38
|
-
*/
|
|
39
|
-
const relationshipWithheldWarning = (expressId, type) => `Relationship #${expressId} (${type}) was withheld from the export: it names at least one entity that has no line in this export, in a slot with no spelling for an omitted reference (a single-valued attribute, or a set whose every member is omitted). Anything else that relationship associated is no longer associated in the output.`;
|
|
7
|
+
import { createSourceRefReader } from './source-ref-bounds.js';
|
|
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';
|
|
40
18
|
/**
|
|
41
19
|
* IFC STEP file exporter
|
|
42
20
|
*/
|
|
@@ -48,7 +26,7 @@ export class StepExporter {
|
|
|
48
26
|
/**
|
|
49
27
|
* The owner-history memos the property-set and quantity-set generators read.
|
|
50
28
|
*
|
|
51
|
-
* Owned here and handed to `step-property-
|
|
29
|
+
* Owned here and handed to `step-property-set-readers.ts` BY REFERENCE rather than
|
|
52
30
|
* stored on its context: the reset below is an `export()`-level statement,
|
|
53
31
|
* and the comment there is where "per export, not per exporter" is argued.
|
|
54
32
|
* Moving the storage into a per-export context would make that reset
|
|
@@ -124,738 +102,45 @@ export class StepExporter {
|
|
|
124
102
|
? sourceHeader.schemaIdentifiers[0]
|
|
125
103
|
: schema;
|
|
126
104
|
// The one construction site for the state this export shares across its
|
|
127
|
-
// seven phases.
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
108
|
+
//
|
|
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,
|
|
133
118
|
schema,
|
|
134
119
|
sourceSchema,
|
|
135
120
|
converting,
|
|
121
|
+
applyMutations,
|
|
122
|
+
excludeGeometry,
|
|
136
123
|
sourceHeader,
|
|
137
124
|
schemaToken,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
implementationLevel: sourceHeader?.implementationLevel,
|
|
158
|
-
author: options.author ?? sourceHeader?.author,
|
|
159
|
-
organization: options.organization ?? sourceHeader?.organization,
|
|
160
|
-
// preprocessor_version = the tool that WROTE this file (ifc-lite);
|
|
161
|
-
// originating_system keeps the source authoring tool so it isn't erased.
|
|
162
|
-
preprocessorVersion: options.application ?? 'ifc-lite',
|
|
163
|
-
originatingSystem: sourceHeader?.originatingSystem,
|
|
164
|
-
authorization: sourceHeader?.authorization,
|
|
165
|
-
application: options.application ?? 'ifc-lite',
|
|
166
|
-
filename: options.filename ?? 'export.ifc',
|
|
167
|
-
timeStamp: options.timeStamp,
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
// The one authority for exists / class / deleted, overlay first and source
|
|
171
|
-
// buffer second. Every pass below asks this instead of `this.dataStore`,
|
|
172
|
-
// which answers only for the file as parsed (#2012).
|
|
173
|
-
effective: getEffectiveEntityIndex(this.dataStore, this.mutationView, applyMutations),
|
|
174
|
-
// Does this id belong to an entity the OVERLAY created (`createEntity` /
|
|
175
|
-
// `store.addEntity`) rather than to a record in the source buffer? Such an
|
|
176
|
-
// entity has no source bytes, so the source-iteration pass below never sees
|
|
177
|
-
// it and the new-entities pass at the end owns its line entirely (#2006).
|
|
178
|
-
isOverlayCreated: (entityId) => pass.effective.isOverlayCreated(entityId),
|
|
179
|
-
// Does this record describe a line this export can actually READ out of the
|
|
180
|
-
// source? One predicate for every byte-range gate below, so they cannot
|
|
181
|
-
// disagree — see `source-ref-bounds.ts` for the corrupt file the weaker
|
|
182
|
-
// "is there a source / does the ref claim bytes" pair let through (#2491).
|
|
183
|
-
isReadableSourceRef: createSourceRefReader(this.dataStore.source),
|
|
184
|
-
// Build visible-only closure if requested. Classification, the closure walk
|
|
185
|
-
// and the style pass all run over the EFFECTIVE index: an overlay-created
|
|
186
|
-
// product becomes a root by the same type rules as a parsed one, the walk
|
|
187
|
-
// follows its authored references into the geometry it alone owns, and a
|
|
188
|
-
// tombstoned entity is simply not there. Run over the source buffer, a
|
|
189
|
-
// created wall could never be a root and nothing referenced it, so
|
|
190
|
-
// `visibleOnly` wrote a file without it and said nothing (#2012).
|
|
191
|
-
//
|
|
192
|
-
// Computed here, ahead of the modification-count passes below, because
|
|
193
|
-
// `hasEmittableHostBytes` needs it: a source-backed host EXCLUDED by
|
|
194
|
-
// `visibleOnly` never gets its line written by the source-iteration pass
|
|
195
|
-
// either, so counting it as "modified" would make the header claim a
|
|
196
|
-
// change the DATA section does not contain (CodeRabbit finding on #2414).
|
|
197
|
-
allowedEntityIds: null,
|
|
198
|
-
// Populated alongside `allowedEntityIds` below. `getVisibleEntityIds`
|
|
199
|
-
// excludes a hidden PRODUCT's own line from the closure, but `IFCREL*` is
|
|
200
|
-
// an unconditional root a few lines down and its bytes are copied verbatim
|
|
201
|
-
// by the source-iteration pass — nothing there filters a `#N` the closure
|
|
202
|
-
// just excluded out of the relationship's own attribute list. Kept because
|
|
203
|
-
// the closure walk's `isRefExcludedDuringClosureWalk` needs a notion of
|
|
204
|
-
// "hidden" that does not read `allowedEntityIds` — the set that walk is
|
|
205
|
-
// producing (#2398). The two OUTPUT passes no longer read this directly:
|
|
206
|
-
// they filter on `isOmittedFromOutput`, which subsumes it via
|
|
207
|
-
// `allowedEntityIds`.
|
|
208
|
-
hiddenProductIds: null,
|
|
209
|
-
// A relationship can name an excluded entity two ways that have nothing
|
|
210
|
-
// to do with each other: a `visibleOnly` hidden PRODUCT (`hiddenProductIds`,
|
|
211
|
-
// below), and a TOMBSTONED one — `editor.removeEntity` on a related object
|
|
212
|
-
// named by a relationship the deletion sweep below does not reach (that
|
|
213
|
-
// sweep only withholds an `IfcRelDefinesByProperties` when EVERY related
|
|
214
|
-
// object is gone, and only for that one relationship class). Left alone, a
|
|
215
|
-
// relationship still naming a deleted entity ships the identical `#N` with
|
|
216
|
-
// no `#N=` line, on a path with no `visibleOnly` involved at all (#2398).
|
|
217
|
-
// `effective.isDeleted` answers for every id, not just a precomputed set,
|
|
218
|
-
// so this predicate covers both sources without a second exclusion set.
|
|
219
|
-
//
|
|
220
|
-
// Declared here, ahead of the closure walk below, and passed into
|
|
221
|
-
// `collectReferencedEntityIds` as its `isRefExcluded` — rather than the
|
|
222
|
-
// walk inventing its own `!entityIndex.has` proxy for "deleted" that could
|
|
223
|
-
// disagree on an id that never existed in the file at all
|
|
224
|
-
// (maintainer-found regression on #2637: such an id blocked the bridge but
|
|
225
|
-
// did not stop the relationship's own line from shipping, dropping a
|
|
226
|
-
// VISIBLE sibling's pset while adding a fresh dangling ref). A closure over
|
|
227
|
-
// `pass.hiddenProductIds`, not a value snapshot — correct because nothing
|
|
228
|
-
// reads it before the closure walk assigns it just below.
|
|
229
|
-
//
|
|
230
|
-
// ## Why this is NOT the predicate the OUTPUT-line filter uses
|
|
231
|
-
//
|
|
232
|
-
// The name says walk, and only walk. The two passes that write a
|
|
233
|
-
// relationship's line ask `isOmittedFromOutput` (further below, derived
|
|
234
|
-
// from `pass.willBeEmitted`), which is strictly stronger — it also answers
|
|
235
|
-
// for the closure, for an unreadable source ref and for a geometry
|
|
236
|
-
// exclusion.
|
|
237
|
-
//
|
|
238
|
-
// This one CANNOT be `willBeEmitted`, and the difference is structural
|
|
239
|
-
// rather than stylistic: `willBeEmitted`'s first act is to consult
|
|
240
|
-
// `allowedEntityIds`, and `allowedEntityIds` is precisely what the call
|
|
241
|
-
// below is computing. Wiring it in here is circular: it would answer "not
|
|
242
|
-
// in the closure" as `false` while the closure is still being built and
|
|
243
|
-
// `true` for the same id afterwards.
|
|
244
|
-
//
|
|
245
|
-
// That is a genuine departure from the contract #2637 was closed on —
|
|
246
|
-
// `reference-collector.ts` still documents the bridge as taking the
|
|
247
|
-
// caller's OWN output predicate, "not two expressions that happened to
|
|
248
|
-
// agree". It has an OBSERVABLE consequence, not just a naming one: for an
|
|
249
|
-
// unreadable source ref this admits, the walk bridges through a
|
|
250
|
-
// relationship the output then withholds, leaving the relationship's other
|
|
251
|
-
// target in the closure with nothing naming it — an orphan, pinned by
|
|
252
|
-
// `unreadable-ref-dangling.test.ts` ("walk and output predicates diverge").
|
|
253
|
-
// The reverse direction is closed: every id this excludes,
|
|
254
|
-
// `isOmittedFromOutput` excludes too, so the #2548 leak cannot return.
|
|
255
|
-
isRefExcludedDuringClosureWalk: (id) => (pass.hiddenProductIds !== null && pass.hiddenProductIds.has(id))
|
|
256
|
-
|| pass.effective.isDeleted(id),
|
|
257
|
-
// Will THIS entity's own line ever land in the file? The same byte-range
|
|
258
|
-
// test `willBeEmitted` uses (defined further below) and the source-
|
|
259
|
-
// iteration pass's own skip at `entityRef.byteLength === 0` — a source
|
|
260
|
-
// entity with no bytes (a point-cloud / GLB "entity" from
|
|
261
|
-
// `createSyntheticDataStore`, not an overlay-created one) never gets a
|
|
262
|
-
// defining line written, source-iteration or otherwise, so a pset/attribute
|
|
263
|
-
// edit against it must not count as a modification either: the header
|
|
264
|
-
// would describe a change the file does not contain (out-of-scope finding
|
|
265
|
-
// in #2398). Also excludes a source-backed host the visible-only closure
|
|
266
|
-
// above drops — same reasoning, different reason the line never lands.
|
|
267
|
-
//
|
|
268
|
-
// And, like `willBeEmitted` below, excludes a geometry-classified SOURCE
|
|
269
|
-
// host under `includeGeometry: false`: the source-iteration pass's own
|
|
270
|
-
// `isGeometryEntity` skip (further below) drops that line too, so this
|
|
271
|
-
// predicate must agree or a geometry entity's attribute edit inflates the
|
|
272
|
-
// count over an omitted line (CodeRabbit finding on #2414). Guarded by
|
|
273
|
-
// `!deltaOnly` for the same reason `willBeEmitted` is: under `deltaOnly`
|
|
274
|
-
// the source-iteration pass — and its geometry skip — never runs at all,
|
|
275
|
-
// so a source entity's line is assumed to already exist in the file being
|
|
276
|
-
// patched, geometry or not.
|
|
277
|
-
isGeometryExcluded: (entityId, recordType) => excludeGeometry
|
|
278
|
-
&& this.isGeometryEntity(pass.effective.effectiveType(entityId, recordType)),
|
|
279
|
-
hasEmittableHostBytes: (entityId) => {
|
|
280
|
-
if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(entityId))
|
|
281
|
-
return false;
|
|
282
|
-
const ref = pass.effective.get(entityId);
|
|
283
|
-
// The ref must be READABLE, not merely non-empty: a range this source
|
|
284
|
-
// cannot address decodes to the empty string, which used to be pushed
|
|
285
|
-
// into the file as a blank line while everything generated FOR the host
|
|
286
|
-
// still named it (#2491).
|
|
287
|
-
if (!ref || !pass.isReadableSourceRef(ref))
|
|
288
|
-
return false;
|
|
289
|
-
if (options.deltaOnly !== true && pass.isGeometryExcluded(entityId, ref.type))
|
|
290
|
-
return false;
|
|
291
|
-
return true;
|
|
292
|
-
},
|
|
293
|
-
/**
|
|
294
|
-
* Will this id have a defining STEP line in the output at all?
|
|
295
|
-
*
|
|
296
|
-
* The predicate is #2030's, and it is the right one: the pset, quantity and
|
|
297
|
-
* type-owned passes below are built from unfiltered mutation history, and
|
|
298
|
-
* what each of them needs to know before emitting an
|
|
299
|
-
* `IFCRELDEFINESBYPROPERTIES` is not "was this deleted" or "is this hidden"
|
|
300
|
-
* but the general question those are two answers to. A relation naming an
|
|
301
|
-
* expressId that never gets written is a dangling reference and an invalid
|
|
302
|
-
* file, whichever route dropped the line.
|
|
303
|
-
*
|
|
304
|
-
* #2030 had to reach for four things to answer it — a tombstone probe, a
|
|
305
|
-
* visibility set, a byte-range test on `completeIndex`, and a `getNewEntity`
|
|
306
|
-
* fallback whose stated purpose was that `deleteEntity` FORGOT an
|
|
307
|
-
* overlay-created entity instead of tombstoning it, so `isDeleted` could not
|
|
308
|
-
* answer for one. That fallback was documented on main as a workaround for
|
|
309
|
-
* exactly the model-level defect this branch fixes: `deleteEntity` now
|
|
310
|
-
* tombstones as well as forgets, so the effective index answers existence
|
|
311
|
-
* for source and overlay ids alike and the workaround collapses into it.
|
|
312
|
-
*
|
|
313
|
-
* The overlay branch does NOT disappear with it, and the distinction matters:
|
|
314
|
-
* `isOverlayCreated` is still load-bearing here, because a live
|
|
315
|
-
* overlay-created entity has no source bytes and would fail the byte-range
|
|
316
|
-
* test that a source record passes. What the tombstone fix removed is the
|
|
317
|
-
* need for that branch to double as a deletion detector.
|
|
318
|
-
*
|
|
319
|
-
* Deliberately unchanged from #2030 for source records under `deltaOnly` /
|
|
320
|
-
* `exportPropertiesOnly`: the source-iteration pass is skipped wholesale in
|
|
321
|
-
* those modes, yet a source entity still answers true here. A delta is a
|
|
322
|
-
* patch against a file that already has the line, not a standalone model.
|
|
323
|
-
*/
|
|
324
|
-
willBeEmitted: (entityId) => {
|
|
325
|
-
if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(entityId))
|
|
326
|
-
return false;
|
|
327
|
-
// Undefined for a tombstoned id and for one neither the file nor the
|
|
328
|
-
// session ever had — a stale mutation must not conjure a relation either.
|
|
329
|
-
const ref = pass.effective.get(entityId);
|
|
330
|
-
if (!ref)
|
|
331
|
-
return false;
|
|
332
|
-
// An overlay-created record carries the placeholder byte range and is
|
|
333
|
-
// written by the new-entities pass; a source record needs real bytes.
|
|
334
|
-
if (pass.effective.isOverlayCreated(entityId)) {
|
|
335
|
-
// The overlay new-entities pass applies its OWN `isGeometryEntity`
|
|
336
|
-
// filter unconditionally — deltaOnly or not (see the comment at that
|
|
337
|
-
// loop, further below) — so this branch mirrors it without the
|
|
338
|
-
// deltaOnly carve-out the source branch gets.
|
|
339
|
-
return !pass.isGeometryExcluded(entityId, ref.type);
|
|
340
|
-
}
|
|
341
|
-
// Same readability test as `hasEmittableHostBytes`, and for the reason
|
|
342
|
-
// that predicate names: a ref this source cannot address is not a line
|
|
343
|
-
// this export can write, so nothing may be generated naming it (#2491).
|
|
344
|
-
if (!pass.isReadableSourceRef(ref))
|
|
345
|
-
return false;
|
|
346
|
-
// Mirrors `hasEmittableHostBytes`: under `deltaOnly` the source-
|
|
347
|
-
// iteration pass — and its geometry skip — never runs, so a source
|
|
348
|
-
// entity's line is assumed to already exist in the file being patched.
|
|
349
|
-
if (options.deltaOnly === true)
|
|
350
|
-
return true;
|
|
351
|
-
return !pass.isGeometryExcluded(entityId, ref.type);
|
|
352
|
-
},
|
|
353
|
-
// Under `deltaOnly` a nomination only becomes a count once some pass has
|
|
354
|
-
// actually written content that delivers THAT KIND of edit for the host —
|
|
355
|
-
// see `delta-modification-ledger.ts` for why the two are not the same event
|
|
356
|
-
// in that mode, and why the pair is (entity, kind) rather than the entity
|
|
357
|
-
// (#2462).
|
|
358
|
-
modifications: createModificationLedger(options.deltaOnly === true),
|
|
359
|
-
/**
|
|
360
|
-
* Hosts whose in-place named-attribute edits a FULL export may count, per
|
|
361
|
-
* kind. Filled by the collection passes below and read by the two passes
|
|
362
|
-
* that write a rewritten source line — see `in-place-nomination.ts` for why
|
|
363
|
-
* the nomination waits for the rewrite in this mode and not under
|
|
364
|
-
* `deltaOnly` (#2483).
|
|
365
|
-
*/
|
|
366
|
-
inPlaceNominees: {
|
|
367
|
-
attribute: new Set(),
|
|
368
|
-
georeferencing: new Set(),
|
|
369
|
-
},
|
|
370
|
-
// Collect entities that need to be modified or created
|
|
371
|
-
modifiedEntities: new Set(),
|
|
372
|
-
modifiedAttributes: new Map(),
|
|
373
|
-
newPropertySets: [],
|
|
374
|
-
newQuantitySets: [],
|
|
375
|
-
typeOwnedPsetNamesByEntity: new Map(),
|
|
376
|
-
typeOwnedPsetIdsByEntity: new Map(),
|
|
377
|
-
rewrittenEntityIds: new Set(),
|
|
378
|
-
rewrittenEntityLines: new Map(),
|
|
379
|
-
/** HasPropertySets slot value for an OVERLAY-CREATED type object, applied
|
|
380
|
-
* by the new-entities pass (there is no source line to rewrite). */
|
|
381
|
-
overlayTypeOwnedPsets: new Map(),
|
|
382
|
-
// Track property set IDs and relationship IDs to skip
|
|
383
|
-
skipPropertySetIds: new Set(),
|
|
384
|
-
skipRelationshipIds: new Set(),
|
|
385
|
-
// Written by the georeferencing pass and read again by the final
|
|
386
|
-
// assembly, which is why they are pass state and not phase locals.
|
|
387
|
-
newGeorefLines: [],
|
|
388
|
-
warnings: [],
|
|
389
|
-
};
|
|
390
|
-
if (options.visibleOnly && this.dataStore.source) {
|
|
391
|
-
const visible = getVisibleEntityIds(this.dataStore, options.hiddenEntityIds ?? new Set(), options.isolatedEntityIds ?? null, pass.effective);
|
|
392
|
-
pass.hiddenProductIds = visible.hiddenProductIds;
|
|
393
|
-
pass.allowedEntityIds = collectReferencedEntityIds(visible.roots, this.dataStore.source, pass.effective, visible.hiddenProductIds, pass.isRefExcludedDuringClosureWalk);
|
|
394
|
-
// Second pass: collect IFCSTYLEDITEM entities that reference included
|
|
395
|
-
// geometry. Styled items reference geometry items but nothing references
|
|
396
|
-
// them back, so the forward closure misses them.
|
|
397
|
-
collectStyleEntities(pass.allowedEntityIds, this.dataStore.source, { byId: pass.effective, byType: pass.effective.byType });
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* "Does this model hold a record whose bytes this export cannot read?" —
|
|
401
|
-
* the one disjunct of {@link mayNameOmittedRefs} that is not already a
|
|
402
|
-
* value in hand, so it is a function and called last, behind `||`.
|
|
403
|
-
*
|
|
404
|
-
* Scans the EFFECTIVE index, and that is a requirement rather than an
|
|
405
|
-
* implementation detail: it has to cover the id space
|
|
406
|
-
* `isOmittedFromOutput` answers over, and an unreadable record can live in
|
|
407
|
-
* `deferredEntityIndex` — the secondary index `getCompleteEntityIndex`
|
|
408
|
-
* exists to merge — and nowhere in `entityIndex.byId`. Scanning `byId`,
|
|
409
|
-
* the obvious cheaper source, was measured to leave the gate false and
|
|
410
|
-
* ship the dangling ref; `relationship-filter-gate.test.ts` pins the
|
|
411
|
-
* merged scan behaviourally so that shortcut cannot come back as an
|
|
412
|
-
* optimisation.
|
|
413
|
-
*
|
|
414
|
-
* Reads the ref ITERATION yields — what the source-iteration pass's own
|
|
415
|
-
* skip reads — rather than re-asking `effective.get(id)` per id as
|
|
416
|
-
* `willBeEmitted` does, which on the largest files would cost a binary
|
|
417
|
-
* search and an allocation per entity and defeat the point of the gate.
|
|
418
|
-
* Every index here keeps the two in step by construction:
|
|
419
|
-
* `CompactEntityIndex` serves `get`, `has` and iteration from one pair of
|
|
420
|
-
* `Uint32Array`s, a `Map` trivially agrees, the merged deferred view is
|
|
421
|
-
* `byId.get ?? deferred.get` over `yield* byId; yield* deferred`, and
|
|
422
|
-
* `OverlayIndex` filters both by one tombstone set. An index whose `has`
|
|
423
|
-
* accepted an id its iteration never yields would defeat this — and would
|
|
424
|
-
* equally defeat the source-iteration pass's skip, so that file is broken
|
|
425
|
-
* either way; nothing in the repo builds one.
|
|
426
|
-
*
|
|
427
|
-
* Not short-circuited on `overlayActive`: an overlay-created record carries
|
|
428
|
-
* `(OVERLAY_BYTE_OFFSET, 0)` and so counts as unreadable here, which would
|
|
429
|
-
* make this always answer true once an overlay exists. Harmless —
|
|
430
|
-
* `overlayActive` is an earlier disjunct, so this never runs then — and
|
|
431
|
-
* correct if it ever did.
|
|
432
|
-
*
|
|
433
|
-
* ## Why a standalone pass rather than a value off the index
|
|
434
|
-
*
|
|
435
|
-
* Measured: 12.0 ms of a 470 ms export at 714,485 entities (2.55%), one
|
|
436
|
-
* call, whole index walked because a well-formed model gives it nothing to
|
|
437
|
-
* short-circuit on. The cheaper shape was prototyped and is 13x faster —
|
|
438
|
-
* `min(byteLength)` and `max(byteOffset + byteLength)` over
|
|
439
|
-
* `CompactEntityIndex`'s own `Uint32Array`s answer "is every ref readable
|
|
440
|
-
* within `extent`" exactly and allocation-free in 0.74 ms — and was not
|
|
441
|
-
* taken, because 11 ms does not buy what it costs.
|
|
442
|
-
*
|
|
443
|
-
* It could only stand in FRONT of this loop, never replace it:
|
|
444
|
-
* `EntityByIdIndex` is a structural type and plain `Map`s satisfy it
|
|
445
|
-
* (`synthetic-data-store.ts` builds one), so the walk stays for those. That
|
|
446
|
-
* makes it a second implementation of one predicate across a package
|
|
447
|
-
* boundary — the defect class #2637, #2668 and this gate are all instances
|
|
448
|
-
* of. And storing it at construction is the invariant
|
|
449
|
-
* `source-ref-bounds.ts` exists to delete: `CompactEntityIndex` is built by
|
|
450
|
-
* its builder, by `compactEntityIndexFromColumns` in the transport, and by
|
|
451
|
-
* embedders, so a value one producer writes is a value the next can skip,
|
|
452
|
-
* whereas testing the ref where it is READ cannot be bypassed. If 2.5% ever
|
|
453
|
-
* has to go, the safe shape is a memoized derivation the index computes
|
|
454
|
-
* from its own arrays on demand — not a field set at build time.
|
|
455
|
-
*/
|
|
456
|
-
const hasAnyUnreadableSourceRef = () => {
|
|
457
|
-
for (const [, ref] of pass.effective) {
|
|
458
|
-
if (!pass.isReadableSourceRef(ref))
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
return false;
|
|
462
|
-
};
|
|
463
|
-
// Process mutations if we have a mutation view
|
|
464
|
-
if (this.mutationView && applyMutations) {
|
|
465
|
-
const mutations = this.mutationView.getMutations();
|
|
466
|
-
// Attribute values come from the *overlay*, never from the mutation
|
|
467
|
-
// history. The history is append-only and undo writes its reverse edit
|
|
468
|
-
// with `skipHistory: true`, so a superseded UPDATE_ATTRIBUTE record keeps
|
|
469
|
-
// its stale `newValue` forever — replaying it resurrects edits the user
|
|
470
|
-
// undid (#1957). The overlay is what the editor shows, and it is already
|
|
471
|
-
// the source for psets, quantities, positional attributes and retypes
|
|
472
|
-
// below, so attributes were the sole outlier.
|
|
473
|
-
for (const [entityId, attrs] of this.mutationView.getAttributeMutationsByEntity()) {
|
|
474
|
-
pass.modifiedEntities.add(entityId);
|
|
475
|
-
let target = pass.modifiedAttributes.get(entityId);
|
|
476
|
-
if (!target) {
|
|
477
|
-
target = new Map();
|
|
478
|
-
pass.modifiedAttributes.set(entityId, target);
|
|
479
|
-
}
|
|
480
|
-
for (const [name, value] of attrs)
|
|
481
|
-
target.set(name, value);
|
|
482
|
-
}
|
|
483
|
-
// Group mutations by entity, separating property vs quantity mutations
|
|
484
|
-
const entityPropMutations = new Map();
|
|
485
|
-
const entityQuantMutations = new Map();
|
|
486
|
-
for (const mutation of mutations) {
|
|
487
|
-
// Handled above, off the overlay. Skipped explicitly because an
|
|
488
|
-
// UPDATE_ATTRIBUTE record can also carry a `psetName` (georef fields
|
|
489
|
-
// encode their target entity there) and must not be mistaken for a
|
|
490
|
-
// property-set edit.
|
|
491
|
-
if (mutation.type === 'UPDATE_ATTRIBUTE')
|
|
492
|
-
continue;
|
|
493
|
-
if (!mutation.psetName)
|
|
494
|
-
continue;
|
|
495
|
-
const isQuantity = mutation.type === 'CREATE_QUANTITY' || mutation.type === 'UPDATE_QUANTITY'
|
|
496
|
-
|| mutation.type === 'DELETE_QUANTITY' || mutation.type === 'DELETE_QUANTITY_SET';
|
|
497
|
-
const targetMap = isQuantity ? entityQuantMutations : entityPropMutations;
|
|
498
|
-
if (!targetMap.has(mutation.entityId)) {
|
|
499
|
-
targetMap.set(mutation.entityId, new Set());
|
|
500
|
-
}
|
|
501
|
-
targetMap.get(mutation.entityId).add(mutation.psetName);
|
|
502
|
-
}
|
|
503
|
-
// Build a reverse index of IfcRelDefinesByProperties → (relId, psetId)
|
|
504
|
-
// pairs keyed on each related entity. The two property/quantity loops
|
|
505
|
-
// below previously walked every entity in `entityIndex.byId` per
|
|
506
|
-
// modified entity (O(E·N)); the index keeps the per-entity step
|
|
507
|
-
// O(K) where K is the number of rels referencing that entity.
|
|
508
|
-
const { byEntity: relDefinesByEntity, relatedByRel } = buildRelDefinesByPropertiesIndex(this.propertySetContext());
|
|
509
|
-
// A source IfcRelDefinesByProperties whose EVERY related object the
|
|
510
|
-
// session deleted has nothing left to relate, and emitting it leaves a
|
|
511
|
-
// `#id` pointing at a record the export skipped. Dropped only when all of
|
|
512
|
-
// them are gone: a rel that still names a live entity is that entity's
|
|
513
|
-
// only link to its psets, and nothing here rewrites a RelatedObjects list.
|
|
514
|
-
for (const [relId, related] of relatedByRel) {
|
|
515
|
-
if (related.length > 0 && related.every((id) => pass.effective.isDeleted(id))) {
|
|
516
|
-
pass.skipRelationshipIds.add(relId);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
collectPropertyAndQuantitySetMutations(pass, options, { entityPropMutations, entityQuantMutations, relDefinesByEntity }, this.propertySetContext());
|
|
520
|
-
for (const [entityId] of pass.modifiedAttributes) {
|
|
521
|
-
// An overlay-CREATED entity carrying attribute edits is emitted once,
|
|
522
|
-
// by the new-entities pass, and already counted in `newEntityCount`.
|
|
523
|
-
// Counting it here too made the header claim two affected entities for
|
|
524
|
-
// one created-then-renamed wall.
|
|
525
|
-
if (pass.isOverlayCreated(entityId))
|
|
526
|
-
continue;
|
|
527
|
-
// A source entity with no bytes never gets its line rewritten (the
|
|
528
|
-
// source-iteration pass skips it), so an attribute edit against it
|
|
529
|
-
// must not inflate the count either.
|
|
530
|
-
if (!pass.hasEmittableHostBytes(entityId))
|
|
531
|
-
continue;
|
|
532
|
-
// Under `deltaOnly` this only NOMINATES the host's ATTRIBUTE edits:
|
|
533
|
-
// nothing writes an in-place attribute edit into a delta except the
|
|
534
|
-
// type-object line rewrite, so the ledger drops it at settle time
|
|
535
|
-
// unless that pass reports having carried it (#2462). That nomination
|
|
536
|
-
// is deliberately made at INTENT: the per-kind warning exists to NAME
|
|
537
|
-
// an edit the delta could not carry, and an undeliverable edit is
|
|
538
|
-
// exactly the one that must still be named.
|
|
539
|
-
//
|
|
540
|
-
// A FULL export has no such warning, so an edit that resolved to
|
|
541
|
-
// nothing has nothing to say and nothing to claim — it waits for the
|
|
542
|
-
// rewrite instead. `setAttribute` to the value already in the slot, and
|
|
543
|
-
// `setAttribute` naming a slot the class does not declare, both leave
|
|
544
|
-
// the line byte-identical and used to count anyway (#2483).
|
|
545
|
-
//
|
|
546
|
-
// Recorded unconditionally. It used to be skipped for a host that also
|
|
547
|
-
// had a pset or qset edit, because the count was per entity and the
|
|
548
|
-
// other loop had already nominated it — which is exactly what let a
|
|
549
|
-
// pset emission mark the rename delivered and suppress its warning. The
|
|
550
|
-
// ledger de-duplicates the COUNT per entity now, so the two edits can
|
|
551
|
-
// and must be nominated separately.
|
|
552
|
-
pass.inPlaceNominees.attribute.add(entityId);
|
|
553
|
-
if (options.deltaOnly === true)
|
|
554
|
-
pass.modifications.nominate(entityId, 'attribute');
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
// Process georeferencing mutations (only when applyMutations is enabled)
|
|
558
|
-
if (applyMutations && options.georefMutations) {
|
|
559
|
-
applyGeoreferencingMutations(pass, options.georefMutations, this.georefContext(options.deltaOnly === true));
|
|
560
|
-
}
|
|
561
|
-
// If delta only, only export modified entities. Overlay-created entities
|
|
562
|
-
// also count — without this, `createEntity()`-only edits would silently
|
|
563
|
-
// drop out of delta exports.
|
|
564
|
-
const overlayNewEntityCount = (this.mutationView
|
|
565
|
-
&& applyMutations
|
|
566
|
-
&& typeof this.mutationView.getNewEntities === 'function') ? this.mutationView.getNewEntities().length : 0;
|
|
567
|
-
// Georef-only deltas (newGeorefLines populated but no entity changes) must
|
|
568
|
-
// still produce a non-empty DATA section.
|
|
569
|
-
if (options.deltaOnly
|
|
570
|
-
&& pass.modifiedEntities.size === 0
|
|
571
|
-
&& overlayNewEntityCount === 0
|
|
572
|
-
&& pass.newGeorefLines.length === 0) {
|
|
573
|
-
const emptyContent = new TextEncoder().encode(pass.buildHeader(0) + 'DATA;\nENDSEC;\nEND-ISO-10303-21;\n');
|
|
574
|
-
return {
|
|
575
|
-
content: emptyContent,
|
|
576
|
-
stats: {
|
|
577
|
-
entityCount: 0,
|
|
578
|
-
newEntityCount: 0,
|
|
579
|
-
modifiedEntityCount: 0,
|
|
580
|
-
fileSize: emptyContent.byteLength,
|
|
581
|
-
warnings: pass.warnings,
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* "May a line this export writes name `#id`?" — the single predicate both
|
|
587
|
-
* relationship-line filter sites consume, derived from `willBeEmitted`
|
|
588
|
-
* rather than from a second list kept in step with it by hand.
|
|
589
|
-
*
|
|
590
|
-
* DERIVED, not identical, and the gaps are named below rather than glossed:
|
|
591
|
-
* a scope qualifier for ids the file never had, and `deltaOnly`, where
|
|
592
|
-
* `willBeEmitted` answers `true` for a source record whose line this export
|
|
593
|
-
* does not write at all (the source-iteration pass is skipped wholesale in
|
|
594
|
-
* that mode). Nor does this make the CLOSURE WALK agree with either: the
|
|
595
|
-
* walk keeps `isRefExcludedDuringClosureWalk` and diverges from this
|
|
596
|
-
* predicate for an unreadable source ref — see the note on that predicate,
|
|
597
|
-
* and the "walk and output predicates diverge" test.
|
|
598
|
-
*
|
|
599
|
-
* The hand-kept second list is the bug this replaces. `willBeEmitted` recognises
|
|
600
|
-
* seven reasons a line never lands — outside the closure, hidden product,
|
|
601
|
-
* tombstoned, never existed, unreadable source ref (#2491), geometry
|
|
602
|
-
* excluded by options, and the `deltaOnly` carve-out — while the filter
|
|
603
|
-
* used to consume `(hiddenProductIds !== null && hiddenProductIds.has(id))
|
|
604
|
-
* || effective.isDeleted(id)`, which answered for two: hidden product, and
|
|
605
|
-
* tombstoned. Notably NOT "never existed" — that one is deliberately out of
|
|
606
|
-
* scope for the filter even now, for the reason under the qualifier heading
|
|
607
|
-
* below. The gap was live: on a PLAIN full export, with no `visibleOnly`,
|
|
608
|
-
* no deletions and no overlay, an unreadable ref made the source-iteration
|
|
609
|
-
* pass skip an entity's line while an `IFCREL*` naming it shipped verbatim,
|
|
610
|
-
* dangling.
|
|
611
|
-
*
|
|
612
|
-
* Deriving the filter from `willBeEmitted` is also what fixed the
|
|
613
|
-
* `mayNameExcludedRefs` gate that stands in front of both call sites. That
|
|
614
|
-
* gate used to be a SECOND, shorter enumeration of the same reasons
|
|
615
|
-
* (hidden products exist, or an overlay is active) and answered `false` for
|
|
616
|
-
* exactly the unreadable-ref export above, so the filter never ran at all.
|
|
617
|
-
* It is now {@link mayNameOmittedRefs} — see there for why a gate is kept
|
|
618
|
-
* at all (running the filter on every `IFCREL*` line costs +13% of a
|
|
619
|
-
* 714k-entity export) and for the enumeration it has to cover.
|
|
620
|
-
*
|
|
621
|
-
* ## The one qualifier on top of `willBeEmitted`
|
|
622
|
-
*
|
|
623
|
-
* `willBeEmitted` answers NO for an id neither the file nor the session
|
|
624
|
-
* ever had, which is right for its own job — nothing GENERATED may name an
|
|
625
|
-
* id that does not exist. It is the wrong answer for rewriting a SOURCE
|
|
626
|
-
* line, and the difference is whose bug it is. A `#999` already sitting in
|
|
627
|
-
* a relationship's `OwnerHistory` slot in the input file is a dangling ref
|
|
628
|
-
* this export did not create and cannot repair; `filterHiddenRefsFromRelationshipLine`
|
|
629
|
-
* withholds a whole relationship when an excluded id is in a bare scalar,
|
|
630
|
-
* so treating it as an exclusion would DELETE a visible element's pset over
|
|
631
|
-
* somebody else's corrupt file. That is the harm #2637 was about, and
|
|
632
|
-
* `step-exporter.test.ts` states the position out loud: a pre-existing
|
|
633
|
-
* dangling ref is out of scope and ships as it arrived.
|
|
634
|
-
*
|
|
635
|
-
* So the filter asks the narrower question: is `#id` an entity this model
|
|
636
|
-
* HAS, that this export is nonetheless not writing? `effective.has` is
|
|
637
|
-
* false for a tombstone, hence the explicit `isDeleted` arm — deleting an
|
|
638
|
-
* entity IS this session's doing and must be filtered.
|
|
639
|
-
*
|
|
640
|
-
* This is a scope qualifier, not a second enumeration of omission reasons:
|
|
641
|
-
* an eighth reason added to `willBeEmitted` still reaches the filter with
|
|
642
|
-
* no edit here.
|
|
643
|
-
*
|
|
644
|
-
* ## What the filter can and cannot reach
|
|
645
|
-
*
|
|
646
|
-
* Only `IFCREL*` lines. A `#N` named from a product's `Representation` or
|
|
647
|
-
* `ObjectPlacement` slot is not touched, so `includeGeometry:false` — a
|
|
648
|
-
* reason `willBeEmitted` does answer for — produces the same dangling refs
|
|
649
|
-
* with this predicate as without it. Measured on `tests/models/AB22.ifc`:
|
|
650
|
-
* 80 dangling refs before and after, output byte-identical but for the
|
|
651
|
-
* header timestamp.
|
|
652
|
-
*
|
|
653
|
-
* ## Withholding is not free
|
|
654
|
-
*
|
|
655
|
-
* When the omitted id sits in a single-valued slot, or is a set's only
|
|
656
|
-
* member, `filterHiddenRefsFromRelationshipLine` withholds the WHOLE
|
|
657
|
-
* relationship — so an entity that relationship also named loses the
|
|
658
|
-
* association, on a plain full export with no options set. That is why the
|
|
659
|
-
* call sites push {@link relationshipWithheldWarning}.
|
|
660
|
-
*
|
|
661
|
-
* See `unreadable-ref-dangling.test.ts` for the reproduction. #2637 is the
|
|
662
|
-
* prior instance of this class, which took seven rounds because the same
|
|
663
|
-
* decision was recomputed per call site.
|
|
664
|
-
*/
|
|
665
|
-
const isOmittedFromOutput = (id) => (pass.effective.has(id) || pass.effective.isDeleted(id)) && !pass.willBeEmitted(id);
|
|
666
|
-
/**
|
|
667
|
-
* "Can ANY id be omitted from this export at all?" — the precondition both
|
|
668
|
-
* `IFCREL*` filter sites are gated on, so the common export pays nothing.
|
|
669
|
-
*
|
|
670
|
-
* ## Why a gate exists
|
|
671
|
-
*
|
|
672
|
-
* Running `filterHiddenRefsFromRelationshipLine` on every `IFCREL*` line
|
|
673
|
-
* costs a re-parse of that line's attribute list, and a large model is
|
|
674
|
-
* mostly relationships. Measured on `tests/models/ara3d/schependomlaan.ifc`
|
|
675
|
-
* (714,485 entities, 21 interleaved reps in randomised order): 463 ms
|
|
676
|
-
* median with this gate false versus 523 ms filtering unconditionally,
|
|
677
|
-
* **+13%**. That is a real price paid on every export to protect a state
|
|
678
|
-
* most exports are not in. With the gate, the same export is 475 ms, +2.7%,
|
|
679
|
-
* all of it the fourth disjunct's one pass.
|
|
680
|
-
*
|
|
681
|
-
* ## Why THIS gate, and not the one that shipped before
|
|
682
|
-
*
|
|
683
|
-
* The gate this replaces was a second, hand-kept enumeration of "reasons an
|
|
684
|
-
* entity might be excluded", and it went stale exactly as such lists do: it
|
|
685
|
-
* named hidden products and the overlay and knew nothing about an unreadable
|
|
686
|
-
* source ref, so the bug this branch fixes reached the output with the
|
|
687
|
-
* filter switched off. A cheap gate is safe only as an OVER-APPROXIMATION of
|
|
688
|
-
* `isOmittedFromOutput` that can be checked against `willBeEmitted` branch
|
|
689
|
-
* by branch — so every branch is listed, with the disjunct that covers it:
|
|
690
|
-
*
|
|
691
|
-
* | `willBeEmitted` answers NO at | covered by |
|
|
692
|
-
* |----------------------------------------------|-------------------------------|
|
|
693
|
-
* | `allowedEntityIds !== null && !has(id)` | `allowedEntityIds !== null` |
|
|
694
|
-
* | `!ref`, because the overlay tombstoned `id` | `overlayActive` |
|
|
695
|
-
* | overlay-created, geometry excluded | `overlayActive` |
|
|
696
|
-
* | `!isReadableSourceRef(ref)` | `hasAnyUnreadableSourceRef()` |
|
|
697
|
-
* | source-backed, geometry excluded | `excludeGeometry` |
|
|
698
|
-
* | `!ref`, because `id` never existed | out of scope (below) |
|
|
699
|
-
* | `!ref` while `has(id)` is TRUE | nothing (below) |
|
|
700
|
-
*
|
|
701
|
-
* "Never existed" needs no disjunct: `isOmittedFromOutput`'s own
|
|
702
|
-
* `(has || isDeleted)` qualifier already drops it, deliberately — a
|
|
703
|
-
* pre-existing dangling ref in somebody else's file is not this export's to
|
|
704
|
-
* repair (see that predicate's note).
|
|
705
|
-
*
|
|
706
|
-
* The last row is a real hole and is stated rather than hidden: an index
|
|
707
|
-
* that answers `has(id)` for an id its iteration never yields makes
|
|
708
|
-
* `isOmittedFromOutput` true with no disjunct true. It needs an index whose
|
|
709
|
-
* `has`, `get` and iteration disagree, which nothing in the repo builds and
|
|
710
|
-
* which would already break the source-iteration pass's own skip — see
|
|
711
|
-
* {@link hasAnyUnreadableSourceRef}, which rests on the same agreement.
|
|
712
|
-
*
|
|
713
|
-
* Three of the four disjuncts are reads of values this export already
|
|
714
|
-
* computed. Only the fourth costs anything, and it short-circuits: `||`
|
|
715
|
-
* evaluates it solely when the other three are false, i.e. only for an
|
|
716
|
-
* export that has nothing else to filter for.
|
|
717
|
-
*
|
|
718
|
-
* ## The two spellings that are deliberately NOT the obvious ones
|
|
719
|
-
*
|
|
720
|
-
* `allowedEntityIds !== null`, not `options.visibleOnly === true`. Not the
|
|
721
|
-
* same test: the closure is built under `if (options.visibleOnly &&
|
|
722
|
-
* this.dataStore.source)`, which is TRUTHY rather than `=== true`, and which
|
|
723
|
-
* is a SECOND read of the caller's object. A plain-JS caller of this
|
|
724
|
-
* published package passing `visibleOnly: 1` — or a `get visibleOnly()` that
|
|
725
|
-
* answers `true` once — built the closure while the gate read false and
|
|
726
|
-
* shipped a relationship naming an entity outside it. Executed, not
|
|
727
|
-
* reasoned: 192 of an 800-case sweep over `visibleOnly`/`hidden`/`isolated`
|
|
728
|
-
* combinations shipped a dangling ref against the `=== true` spelling, 0
|
|
729
|
-
* against this one. Reading the state the walk PRODUCED cannot disagree with
|
|
730
|
-
* the walk, whatever `options` says afterwards.
|
|
731
|
-
*
|
|
732
|
-
* It is also wider than the `hiddenProductIds.size > 0` the old gate used: a
|
|
733
|
-
* closure exists whenever `visibleOnly` was requested, even with nothing
|
|
734
|
-
* hidden, and can exclude an entity the roots simply never reach. No fixture
|
|
735
|
-
* has produced that case, so the widening is defensive — but a gate that is
|
|
736
|
-
* true too often costs speed on a rare path, while one that is false too
|
|
737
|
-
* rarely ships a corrupt file, and this one costs nothing.
|
|
738
|
-
*
|
|
739
|
-
* `overlayActive` and `excludeGeometry` are the SAME consts the effective
|
|
740
|
-
* index and `isGeometryExcluded` are built from — one read of `options` per
|
|
741
|
-
* question, shared — so those two cannot disagree with the predicate either.
|
|
742
|
-
*/
|
|
743
|
-
const mayNameOmittedRefs = pass.allowedEntityIds !== null
|
|
744
|
-
|| pass.overlayActive
|
|
745
|
-
|| excludeGeometry
|
|
746
|
-
|| hasAnyUnreadableSourceRef();
|
|
747
|
-
// A modified pset is replaced wholesale, which skips ALL of its member atoms.
|
|
748
|
-
// But IFC exporters deduplicate identical Pset_*Common atoms (e.g. one
|
|
749
|
-
// IsExternal IfcPropertySingleValue shared by dozens of psets), so skipping a
|
|
750
|
-
// shared atom would orphan every OTHER pset that still references it, leaving
|
|
751
|
-
// dangling refs and an invalid file. Keep any atom a surviving container needs.
|
|
752
|
-
this.retainSharedAtoms(pass.skipPropertySetIds, pass.allowedEntityIds);
|
|
753
|
-
// Export original entities from source buffer, SKIPPING modified property sets
|
|
754
|
-
if (!options.deltaOnly && this.dataStore.source) {
|
|
755
|
-
const source = this.dataStore.source;
|
|
756
|
-
// Extract existing entities from source. The effective index has already
|
|
757
|
-
// dropped everything the overlay tombstoned, so there is no separate
|
|
758
|
-
// deleted check to forget here.
|
|
759
|
-
for (const [expressId, entityRef] of pass.effective) {
|
|
760
|
-
// Skip overlay-only entities — emitted by the new-entities pass below.
|
|
761
|
-
// A ref this source cannot address is skipped by the same test rather
|
|
762
|
-
// than decoded: `decodeUtf8` clamps such a range and the empty string
|
|
763
|
-
// it returns used to be pushed into the file as a blank line, leaving
|
|
764
|
-
// every generated record that names the host dangling (#2491).
|
|
765
|
-
if (!pass.isReadableSourceRef(entityRef)) {
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
// Skip entities outside the visible closure
|
|
769
|
-
if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(expressId)) {
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
// Skip property sets/relationships that are being replaced
|
|
773
|
-
if (pass.skipPropertySetIds.has(expressId) || pass.skipRelationshipIds.has(expressId)) {
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
// Skip type entities whose HasPropertySets attribute will be rewritten
|
|
777
|
-
if (pass.rewrittenEntityIds.has(expressId)) {
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
// Skip geometry if not included. Classified via `isGeometryExcluded`
|
|
781
|
-
// (which reads the EFFECTIVE type, `effective.effectiveType`) rather
|
|
782
|
-
// than `entityRef.type` directly: a retype can move a record across
|
|
783
|
-
// the geometry boundary in either direction, and this check has to
|
|
784
|
-
// agree with `hasEmittableHostBytes`/`willBeEmitted`'s use of the
|
|
785
|
-
// same predicate — otherwise a wall retyped to `IfcCartesianPoint`
|
|
786
|
-
// still ships its (rewritten) geometry line under
|
|
787
|
-
// `includeGeometry: false`, the exact "predicate must agree" failure
|
|
788
|
-
// this file already guards for the non-retyped case (#2414).
|
|
789
|
-
if (pass.isGeometryExcluded(expressId, entityRef.type)) {
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
// Get original entity text — decodeRange handles SAB-backed
|
|
793
|
-
// sources (Firefox/Chrome reject `TextDecoder.decode()` on a
|
|
794
|
-
// SharedArrayBuffer-backed view; the parser deliberately keeps
|
|
795
|
-
// `source` zero-copy SAB-backed for worker sharing).
|
|
796
|
-
const entityText = decodeRange(source, entityRef.byteOffset, entityRef.byteOffset + entityRef.byteLength);
|
|
797
|
-
// Retype, named attribute edits and positional edits, in that order.
|
|
798
|
-
// Shared verbatim with the type-object `HasPropertySets` rewrite below,
|
|
799
|
-
// which writes the line this pass would otherwise have written.
|
|
800
|
-
const mutated = this.applySourceLineMutations(expressId, entityText, entityRef.type, pass.modifiedAttributes.get(expressId), pass.sourceSchema, pass.overlayActive, (attr, value) => pass.warnings.push(`entity #${expressId}: attribute ${attr} not written - ` +
|
|
801
|
-
`${JSON.stringify(value)} is not a number and the slot is REAL-typed`));
|
|
802
|
-
let nextEntityText = mutated.text;
|
|
803
|
-
// A hidden PRODUCT's own line is already out of the export via
|
|
804
|
-
// `allowedEntityIds`, and a TOMBSTONED entity's via `effective` — this
|
|
805
|
-
// is the relationship that NAMED either one. `IFCREL*` is an
|
|
806
|
-
// unconditional root (see `getVisibleEntityIds`), so its bytes reach
|
|
807
|
-
// here unfiltered even when one of the ids they name was just
|
|
808
|
-
// excluded; left alone that ships a `#N` with no `#N=` line, whether
|
|
809
|
-
// the exclusion came from `visibleOnly` or from a plain deletion
|
|
810
|
-
// (#2398). Checked before the nomination below: a relationship this
|
|
811
|
-
// withholds must not also be counted as a delivered modification.
|
|
812
|
-
//
|
|
813
|
-
// Classified by the EFFECTIVE type (`effective.effectiveType`), not
|
|
814
|
-
// the source's authored type: a retype can move a record across
|
|
815
|
-
// the `IFCREL*` boundary in either direction (`applySourceLineMutations`
|
|
816
|
-
// already rewrote `nextEntityText` to the new class), and this check
|
|
817
|
-
// has to agree with what actually got written, the same way
|
|
818
|
-
// `getVisibleEntityIds` already does for the visibility walk itself.
|
|
819
|
-
const effectiveRelType = pass.effective.effectiveType(expressId, entityRef.type).toUpperCase();
|
|
820
|
-
if (mayNameOmittedRefs && effectiveRelType.startsWith('IFCREL')) {
|
|
821
|
-
const filtered = filterHiddenRefsFromRelationshipLine(nextEntityText, isOmittedFromOutput);
|
|
822
|
-
if (filtered === null) {
|
|
823
|
-
pass.warnings.push(relationshipWithheldWarning(expressId, effectiveRelType));
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
nextEntityText = filtered;
|
|
827
|
-
}
|
|
828
|
-
// A retype or a positional edit that CHANGED the line is what makes
|
|
829
|
-
// this entity count; a named attribute edit was already nominated by
|
|
830
|
-
// the collection pass. Both flags report effect, so retyping an entity
|
|
831
|
-
// to the class it already is — or writing a slot the token it already
|
|
832
|
-
// holds — no longer claims a modification over a line the export left
|
|
833
|
-
// byte-identical. This pass is full-export-only (`deltaOnly` skips it
|
|
834
|
-
// wholesale), so nomination IS emission here and the kinds only have to
|
|
835
|
-
// be right for the entity count — which is per entity, hence unchanged.
|
|
836
|
-
if (mutated.retyped || mutated.positional)
|
|
837
|
-
pass.modifiedEntities.add(expressId);
|
|
838
|
-
if (mutated.retyped)
|
|
839
|
-
pass.modifications.nominate(expressId, 'retype');
|
|
840
|
-
if (mutated.positional)
|
|
841
|
-
pass.modifications.nominate(expressId, 'positional');
|
|
842
|
-
// The named-attribute kinds join them here rather than at their
|
|
843
|
-
// collection sites, for the same reason and on the same signal (#2483).
|
|
844
|
-
// This pass is full-export-only, so there is nothing to gate.
|
|
845
|
-
nominateDeliveredInPlaceEdits(pass.modifications, expressId, mutated, pass.inPlaceNominees);
|
|
846
|
-
// Apply schema conversion if exporting to a different schema version
|
|
847
|
-
if (pass.converting) {
|
|
848
|
-
const converted = convertStepLine(nextEntityText, pass.sourceSchema, pass.schema, options.guidRandom);
|
|
849
|
-
if (converted !== null) {
|
|
850
|
-
pass.entities.push(converted);
|
|
851
|
-
}
|
|
852
|
-
// null means entity should be skipped (no valid representation in target schema)
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
pass.entities.push(nextEntityText);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
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()));
|
|
859
144
|
// Generated property/quantity sets and the type-object `HasPropertySets`
|
|
860
145
|
// rewrite that resolves against them, in that one order (#2475 steps 2b
|
|
861
146
|
// and 2c). `pass.rewrittenEntityLines`, this call's output, is flushed
|
|
@@ -868,132 +153,12 @@ export class StepExporter {
|
|
|
868
153
|
for (const line of pass.newGeorefLines) {
|
|
869
154
|
pass.entities.push(line);
|
|
870
155
|
}
|
|
871
|
-
// Add overlay-created entities (store.addEntity / mutationView.createEntity)
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
//
|
|
875
|
-
// `
|
|
876
|
-
|
|
877
|
-
&& applyMutations
|
|
878
|
-
&& typeof this.mutationView.getNewEntities === 'function') {
|
|
879
|
-
const getTypeMut = typeof this.mutationView.getEntityTypeMutation === 'function'
|
|
880
|
-
? this.mutationView.getEntityTypeMutation.bind(this.mutationView)
|
|
881
|
-
: null;
|
|
882
|
-
for (const entity of this.mutationView.getNewEntities()) {
|
|
883
|
-
// A retyped overlay entity keeps its AUTHORED type on `entity.type`
|
|
884
|
-
// (the overlay typeMutation is the source of truth for the effective
|
|
885
|
-
// class). Resolve the effective class, then re-lay-out the authored
|
|
886
|
-
// attributes from the authored layout up to it.
|
|
887
|
-
const typeMut = getTypeMut ? getTypeMut(entity.expressId) : null;
|
|
888
|
-
const effectiveType = typeMut?.newType ?? entity.type;
|
|
889
|
-
// STEP requires UPPERCASE entity type tokens; the upper-case happens
|
|
890
|
-
// here at the file-format boundary.
|
|
891
|
-
const upperType = effectiveType.toUpperCase();
|
|
892
|
-
if (excludeGeometry && this.isGeometryEntity(upperType)) {
|
|
893
|
-
continue;
|
|
894
|
-
}
|
|
895
|
-
if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(entity.expressId)) {
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
// Re-lay-out by name against the effective class (identity for
|
|
899
|
-
// compatible layouts). Runs whenever a retype intent exists — even a
|
|
900
|
-
// same-class retype, which carries a PredefinedType override
|
|
901
|
-
// (e.g. setEntityType(id, 'IfcColumn', 'PILASTER')).
|
|
902
|
-
let argsText;
|
|
903
|
-
if (typeMut) {
|
|
904
|
-
// Serialize against the AUTHORED layout (`entity.type`); retypeArgTokens
|
|
905
|
-
// then re-lays the tokens out by name up to the effective class.
|
|
906
|
-
const srcTokens = entity.attributes.map((value, i) => serializeAttributeSlot(entity.type, i, value, pass.sourceSchema));
|
|
907
|
-
const { tokens } = retypeArgTokens(srcTokens, entity.type, effectiveType, typeMut.predefinedType ?? null, pass.sourceSchema);
|
|
908
|
-
argsText = tokens.join(',');
|
|
909
|
-
}
|
|
910
|
-
else {
|
|
911
|
-
argsText = serializeEntityArgs(entity.type, entity.attributes, pass.sourceSchema);
|
|
912
|
-
}
|
|
913
|
-
// Edits made AFTER the create live in the overlay, never in the
|
|
914
|
-
// authored payload (#2006). The source-iteration pass applies them to
|
|
915
|
-
// source records via applyAttributeMutations / applyPositionalMutations;
|
|
916
|
-
// an overlay-created entity has no source record, so without this it was
|
|
917
|
-
// written from its creation payload alone and every later
|
|
918
|
-
// `setAttribute` / `setPositionalAttribute` was silently dropped on
|
|
919
|
-
// save — data loss with no error and no warning.
|
|
920
|
-
//
|
|
921
|
-
// Order mirrors the source pass: retype (above) -> named attributes ->
|
|
922
|
-
// positional overrides, all resolved against the EFFECTIVE class.
|
|
923
|
-
const attributeOverrides = pass.modifiedAttributes.get(entity.expressId) ?? null;
|
|
924
|
-
const queuedPositional = typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
925
|
-
? this.mutationView.getPositionalMutationsForEntity(entity.expressId)
|
|
926
|
-
: null;
|
|
927
|
-
// A created TYPE object owns its psets through HasPropertySets, and the
|
|
928
|
-
// ids of the psets this export generated are only known now — so they
|
|
929
|
-
// arrive as one more slot override rather than through the overlay.
|
|
930
|
-
// `has`, not `??`, for the same reason `overlaySlotValue` gives: the
|
|
931
|
-
// stored value is deliberately null when the resolved list is empty.
|
|
932
|
-
const positionalOverrides = pass.overlayTypeOwnedPsets.has(entity.expressId)
|
|
933
|
-
? new Map(queuedPositional).set(HAS_PROPERTY_SETS_SLOT, pass.overlayTypeOwnedPsets.get(entity.expressId) ?? null)
|
|
934
|
-
: queuedPositional;
|
|
935
|
-
if ((attributeOverrides && attributeOverrides.size > 0)
|
|
936
|
-
|| (positionalOverrides && positionalOverrides.size > 0)) {
|
|
937
|
-
argsText = this.applyOverlayEntityOverrides(argsText, upperType, attributeOverrides, positionalOverrides, pass.sourceSchema,
|
|
938
|
-
// Overlay-created entities report a rejected REAL edit exactly as
|
|
939
|
-
// source-backed ones do. Without this the slot was kept and NOTHING
|
|
940
|
-
// was said - the silent discard this whole change exists to
|
|
941
|
-
// prevent, surviving in the one path that had no test.
|
|
942
|
-
(attr, value) => pass.warnings.push(`entity #${entity.expressId}: attribute ${attr} not written - ` +
|
|
943
|
-
`${JSON.stringify(value)} is not a number and the slot is REAL-typed`));
|
|
944
|
-
}
|
|
945
|
-
let line = `#${entity.expressId}=${upperType}(${argsText});`;
|
|
946
|
-
// Same gap as the source-iteration pass, for an overlay-authored
|
|
947
|
-
// relationship instead of a parsed one (#2398).
|
|
948
|
-
//
|
|
949
|
-
// `mayNameOmittedRefs` is provably TRUE wherever this line executes:
|
|
950
|
-
// the block enclosing this pass requires `this.mutationView` and
|
|
951
|
-
// `applyMutations`, which is `pass.overlayActive`, which is one of the
|
|
952
|
-
// gate's own disjuncts. Spelled out anyway so both filter sites read the
|
|
953
|
-
// same — the previous gate's failure was one site's condition drifting
|
|
954
|
-
// from what the filter needed, and a pass reachable without an overlay
|
|
955
|
-
// would otherwise silently need the gate re-derived here.
|
|
956
|
-
if (mayNameOmittedRefs && upperType.startsWith('IFCREL')) {
|
|
957
|
-
line = filterHiddenRefsFromRelationshipLine(line, isOmittedFromOutput);
|
|
958
|
-
if (line === null) {
|
|
959
|
-
pass.warnings.push(relationshipWithheldWarning(entity.expressId, upperType));
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (pass.converting) {
|
|
964
|
-
const converted = convertStepLine(line, pass.sourceSchema, pass.schema, options.guidRandom);
|
|
965
|
-
if (converted !== null) {
|
|
966
|
-
pass.entities.push(converted);
|
|
967
|
-
pass.newEntityCount++;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
else {
|
|
971
|
-
pass.entities.push(line);
|
|
972
|
-
pass.newEntityCount++;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
// Settle the count against what the passes above actually wrote, and say
|
|
977
|
-
// out loud every KIND of edit a delta could not carry, per host. Silence
|
|
978
|
-
// was the other half of #2462: `deltaOnly` skips the source-iteration pass,
|
|
979
|
-
// so an in-place edit to a source entity is not in the file and never was —
|
|
980
|
-
// the header merely used to claim otherwise.
|
|
981
|
-
const { modifiedEntityCount, warnings: deltaWarnings } = pass.modifications.settle();
|
|
982
|
-
pass.warnings.push(...deltaWarnings);
|
|
983
|
-
// Assemble final file as Uint8Array chunks to avoid V8 string length limit.
|
|
984
|
-
// The header is built last so its provenance item reflects the real count.
|
|
985
|
-
const header = pass.buildHeader(pass.newEntityCount + modifiedEntityCount);
|
|
986
|
-
const content = assembleStepBytes(header, pass.entities);
|
|
987
|
-
return {
|
|
988
|
-
content,
|
|
989
|
-
stats: {
|
|
990
|
-
entityCount: pass.entities.length,
|
|
991
|
-
newEntityCount: pass.newEntityCount,
|
|
992
|
-
modifiedEntityCount,
|
|
993
|
-
fileSize: content.byteLength,
|
|
994
|
-
warnings: pass.warnings,
|
|
995
|
-
},
|
|
996
|
-
};
|
|
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);
|
|
997
162
|
}
|
|
998
163
|
/**
|
|
999
164
|
* Async export that yields to the event loop periodically, keeping the
|
|
@@ -1027,297 +192,6 @@ export class StepExporter {
|
|
|
1027
192
|
deltaOnly: true,
|
|
1028
193
|
});
|
|
1029
194
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
* THE mutation pipeline for a line read out of the source buffer: retype,
|
|
1032
|
-
* then named attribute edits, then positional edits.
|
|
1033
|
-
*
|
|
1034
|
-
* **One implementation, two call sites**, and that is the whole point. Two
|
|
1035
|
-
* passes can write the defining line of a source entity — the
|
|
1036
|
-
* source-iteration pass, and the type-object `HasPropertySets` rewrite that
|
|
1037
|
-
* REPLACES it (`rewrittenEntityIds` makes the source pass skip those ids).
|
|
1038
|
-
* The rewrite used to do its own thing (replace slot 5, nothing else), so
|
|
1039
|
-
* every other edit to a type object with a type-owned pset edit was dropped
|
|
1040
|
-
* in silence: first the renames (#2462 follow-up), and after those were
|
|
1041
|
-
* special-cased here, still the retypes and the positional edits. Whatever
|
|
1042
|
-
* the source pass applies, the rewrite has to apply too, or the next edit
|
|
1043
|
-
* kind added to one site goes missing at the other.
|
|
1044
|
-
*
|
|
1045
|
-
* The order is load-bearing:
|
|
1046
|
-
*
|
|
1047
|
-
* - the retype runs FIRST so named attribute edits resolve against the
|
|
1048
|
-
* TARGET class's attribute names, and so positional slots are indexed
|
|
1049
|
-
* into the retyped argument list;
|
|
1050
|
-
* - the `HasPropertySets` replacement (rewrite path only) runs LAST, on
|
|
1051
|
-
* the text this returns. Run it first and a positional edit to slot 5 —
|
|
1052
|
-
* or a retype's argument-list rebuild — overwrites the resolved pset
|
|
1053
|
-
* list with the stale one, which is the same silent drop one slot over.
|
|
1054
|
-
*
|
|
1055
|
-
* The expressId is unchanged by all of this, so geometry / placement /
|
|
1056
|
-
* representation and every IfcRel* reference (keyed by #id) carry over.
|
|
1057
|
-
*
|
|
1058
|
-
* All three flags report EFFECT, not intent — each is the answer to "did this
|
|
1059
|
-
* operation change the line", measured across that operation alone. The count
|
|
1060
|
-
* and the ledger are claims about the FILE, so an edit that resolves to the
|
|
1061
|
-
* text already there has delivered nothing and must not be reported: retyping
|
|
1062
|
-
* an entity to the class it already is, or writing a positional slot the token
|
|
1063
|
-
* it already holds, used to count as a modification and reach the ledger as a
|
|
1064
|
-
* landed edit, over a byte-identical line. Discarded edits read the same way:
|
|
1065
|
-
* `applyAttributeMutations` drops a name its class has no slot for and
|
|
1066
|
-
* `retypeStepLine` returns an unparseable line untouched, and neither is a
|
|
1067
|
-
* modification of anything.
|
|
1068
|
-
*
|
|
1069
|
-
* `retyped` / `positional` matter most in a FULL export, which is where the
|
|
1070
|
-
* two are nominated (their edits have no earlier nomination site); named
|
|
1071
|
-
* attribute edits are nominated by the collection pass and `attributed` only
|
|
1072
|
-
* settles their delivery.
|
|
1073
|
-
*/
|
|
1074
|
-
applySourceLineMutations(expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected) {
|
|
1075
|
-
let text = entityText;
|
|
1076
|
-
let workingType = recordType.toUpperCase();
|
|
1077
|
-
const typeMutation = overlayActive && typeof this.mutationView.getEntityTypeMutation === 'function'
|
|
1078
|
-
? this.mutationView.getEntityTypeMutation(expressId)
|
|
1079
|
-
: null;
|
|
1080
|
-
let retyped = false;
|
|
1081
|
-
if (typeMutation) {
|
|
1082
|
-
const beforeRetype = text;
|
|
1083
|
-
text = retypeStepLine(text, recordType, typeMutation.newType, typeMutation.predefinedType ?? null, sourceSchema);
|
|
1084
|
-
retyped = text !== beforeRetype;
|
|
1085
|
-
// Set even for a no-op retype: the entity IS the target class from here
|
|
1086
|
-
// on, so the named and positional edits below must resolve against it.
|
|
1087
|
-
workingType = typeMutation.newType.toUpperCase();
|
|
1088
|
-
}
|
|
1089
|
-
// `applyAttributeMutations` returns its input UNCHANGED when it wrote
|
|
1090
|
-
// nothing — no slot resolved for any of the names, or the line does not
|
|
1091
|
-
// parse — so comparing is what tells the ledger whether a named attribute
|
|
1092
|
-
// edit was really carried, rather than merely attempted.
|
|
1093
|
-
let attributed = false;
|
|
1094
|
-
if (attributeMutations && attributeMutations.size > 0) {
|
|
1095
|
-
const beforeAttributes = text;
|
|
1096
|
-
text = this.applyAttributeMutations(text, workingType, attributeMutations, sourceSchema, onRejected);
|
|
1097
|
-
attributed = text !== beforeAttributes;
|
|
1098
|
-
}
|
|
1099
|
-
const positionals = overlayActive && typeof this.mutationView.getPositionalMutationsForEntity === 'function'
|
|
1100
|
-
? this.mutationView.getPositionalMutationsForEntity(expressId)
|
|
1101
|
-
: null;
|
|
1102
|
-
let positional = false;
|
|
1103
|
-
if (positionals && positionals.size > 0) {
|
|
1104
|
-
const beforePositionals = text;
|
|
1105
|
-
text = this.applyPositionalMutations(text, positionals, workingType, sourceSchema);
|
|
1106
|
-
positional = text !== beforePositionals;
|
|
1107
|
-
}
|
|
1108
|
-
return { text, attributed, retyped, positional };
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Rewrite root IFC attributes directly on the original STEP entity line.
|
|
1112
|
-
*/
|
|
1113
|
-
applyAttributeMutations(entityText, entityType, attributeMutations, schemaVersion, onRejected) {
|
|
1114
|
-
const openParen = entityText.indexOf('(');
|
|
1115
|
-
const closeParen = entityText.lastIndexOf(');');
|
|
1116
|
-
if (openParen < 0 || closeParen < openParen) {
|
|
1117
|
-
return entityText;
|
|
1118
|
-
}
|
|
1119
|
-
// Cross-schema, not the IFC4 pin: an IFC4X3-only class (IfcCourse, IfcRoad,
|
|
1120
|
-
// IfcBridge, …) resolves no slots under the pin, so every named edit on one
|
|
1121
|
-
// was silently discarded here too. Identical for the 755 pinned classes
|
|
1122
|
-
// that declare attributes — `attribute-slot-types.test.ts` measures that —
|
|
1123
|
-
// so no IFC4 export changes; this only stops dropping edits it used to drop.
|
|
1124
|
-
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
1125
|
-
if (attrNames.length === 0) {
|
|
1126
|
-
return entityText;
|
|
1127
|
-
}
|
|
1128
|
-
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
1129
|
-
// A source line NEVER pads (unlike the overlay-created path): a short
|
|
1130
|
-
// argument list here means the file speaks a different schema, and growing
|
|
1131
|
-
// a record we did not author would corrupt it.
|
|
1132
|
-
let changed = false;
|
|
1133
|
-
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1134
|
-
for (const [attrName, value] of attributeMutations) {
|
|
1135
|
-
const index = attrNames.indexOf(attrName);
|
|
1136
|
-
if (index < 0 || index >= args.length)
|
|
1137
|
-
continue;
|
|
1138
|
-
// The source path shares every `$`-slot hole with the overlay-created
|
|
1139
|
-
// path, because a source record has plenty of `$` slots of its own. Both
|
|
1140
|
-
// go through the one helper below.
|
|
1141
|
-
const serialized = this.serializeNamedAttribute(entityType, index, value, args[index], realSlots);
|
|
1142
|
-
if (serialized === null) {
|
|
1143
|
-
// Slot untouched AND reported. Not counted as a change: claiming a
|
|
1144
|
-
// modification we did not make is the failure this avoids.
|
|
1145
|
-
onRejected?.(attrName, value);
|
|
1146
|
-
continue;
|
|
1147
|
-
}
|
|
1148
|
-
args[index] = serialized;
|
|
1149
|
-
changed = true;
|
|
1150
|
-
}
|
|
1151
|
-
if (!changed) {
|
|
1152
|
-
return entityText;
|
|
1153
|
-
}
|
|
1154
|
-
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
1155
|
-
}
|
|
1156
|
-
/**
|
|
1157
|
-
* Serialize one NAMED attribute override into its slot — the single point
|
|
1158
|
-
* both the source-buffer rewrite and the overlay-created rewrite go through.
|
|
1159
|
-
*
|
|
1160
|
-
* `serializeAttributeValue` decides the STEP form by reading the token being
|
|
1161
|
-
* replaced, which is sound only while that token carries type information. A
|
|
1162
|
-
* `$` slot carries none, and both paths have plenty: a source record's
|
|
1163
|
-
* optional attributes are `$`, and overlay-created records pad missing slots
|
|
1164
|
-
* with `$`. So the declared type decides first, and inference is the fallback
|
|
1165
|
-
* for slots the schema does not classify (references, SELECTs, numerics),
|
|
1166
|
-
* where reading the old token is exactly the right heuristic.
|
|
1167
|
-
*
|
|
1168
|
-
* Before this REAL check existed, "the declared type decides first" was true
|
|
1169
|
-
* for enum/string slots only — a REAL-backed slot (`IfcMapConversion.
|
|
1170
|
-
* OrthogonalHeight`, any other `IfcLengthMeasure`/`IfcReal`-typed attribute)
|
|
1171
|
-
* fell straight to `serializeAttributeValue`'s token inference, which quotes
|
|
1172
|
-
* anything it cannot recognize as numeric. A schema-legal `$` placeholder
|
|
1173
|
-
* carries no digits to recognize, so setting such a field for the first time
|
|
1174
|
-
* wrote `'12345'` in a slot ISO 10303-21 requires to be an unquoted REAL —
|
|
1175
|
-
* silently invalid output (#2724, LTplus-AG/ifc-lite#2475).
|
|
1176
|
-
*/
|
|
1177
|
-
serializeNamedAttribute(entityType, index, value, currentToken, realSlots) {
|
|
1178
|
-
if (getEnumTypedSlots(entityType).has(index))
|
|
1179
|
-
return serializeEnumToken(value);
|
|
1180
|
-
if (getStringTypedSlots(entityType).has(index))
|
|
1181
|
-
return serializeStringSlot(value);
|
|
1182
|
-
if (realSlots.has(index)) {
|
|
1183
|
-
const trimmed = value.trim();
|
|
1184
|
-
if (trimmed === '')
|
|
1185
|
-
return '$';
|
|
1186
|
-
const numberValue = Number(trimmed);
|
|
1187
|
-
if (Number.isFinite(numberValue))
|
|
1188
|
-
return toStepReal(numberValue);
|
|
1189
|
-
// A non-numeric value in a REAL slot used to fall through and be QUOTED,
|
|
1190
|
-
// producing the same ISO 10303-21 violation #2725 exists to prevent
|
|
1191
|
-
// (#2741). `StoreEditor.setAttribute` takes a string, so any UI text
|
|
1192
|
-
// field bound to a georeferencing REAL can deliver one; it does not need
|
|
1193
|
-
// a corrupt file.
|
|
1194
|
-
//
|
|
1195
|
-
// `null` means "leave the slot as the file had it". Simply returning
|
|
1196
|
-
// `currentToken` here would stop the invalid output but SILENTLY DISCARD
|
|
1197
|
-
// the edit - the exporter would then claim a modification it did not
|
|
1198
|
-
// carry, which is the exact misreport #2723/#2724/#2726 were written to
|
|
1199
|
-
// pin. The caller turns this into a warning, so a dropped edit is visible
|
|
1200
|
-
// rather than inferred from absence.
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
return serializeAttributeValue(value, currentToken);
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Apply overlay attribute + positional overrides to an OVERLAY-CREATED
|
|
1207
|
-
* entity's argument list (#2006).
|
|
1208
|
-
*
|
|
1209
|
-
* Distinct from {@link applyAttributeMutations} / {@link applyPositionalMutations},
|
|
1210
|
-
* which rewrite a line read out of the source buffer. Here the whole line is
|
|
1211
|
-
* ours: it was serialized moments ago from the creation payload, so the
|
|
1212
|
-
* argument list is the authoring payload's, not the file's. That difference
|
|
1213
|
-
* is why this PADS — `entity_create` takes whatever positional list the
|
|
1214
|
-
* caller passes, so a wall authored with three arguments still has a real
|
|
1215
|
-
* `Tag` slot at index 7, and dropping the edit because the payload was short
|
|
1216
|
-
* would be the very data loss this fixes. The source-buffer path must not
|
|
1217
|
-
* pad: there a short line means a different schema, and growing a record we
|
|
1218
|
-
* did not author would corrupt it.
|
|
1219
|
-
*
|
|
1220
|
-
* Named and positional overrides resolve to a slot index up front and share
|
|
1221
|
-
* ONE padding rule. Two padding rules on one record is how the next bug
|
|
1222
|
-
* starts, and the argument for padding — the class is fixed at creation time,
|
|
1223
|
-
* so a short payload is partial authoring — never depended on which of the
|
|
1224
|
-
* two APIs queued the edit.
|
|
1225
|
-
*/
|
|
1226
|
-
applyOverlayEntityOverrides(argsText, entityType, attributeOverrides, positionalOverrides, schemaVersion, onRejected) {
|
|
1227
|
-
const args = argsText.length > 0 ? splitTopLevelArgs(argsText) : [];
|
|
1228
|
-
const attrNames = getAttributeNamesAcrossSchemas(entityType);
|
|
1229
|
-
const named = [];
|
|
1230
|
-
for (const [attrName, value] of attributeOverrides ?? []) {
|
|
1231
|
-
const index = attrNames.indexOf(attrName);
|
|
1232
|
-
if (index >= 0)
|
|
1233
|
-
named.push([index, value]);
|
|
1234
|
-
}
|
|
1235
|
-
// Grow to the class's FULL declared arity as soon as any override names a
|
|
1236
|
-
// declared slot the creation payload never reached. Growing only as far as
|
|
1237
|
-
// the edited slot would emit eight arguments for an IfcWall that declares
|
|
1238
|
-
// nine: this parser tolerates the truncated record, a schema-validating
|
|
1239
|
-
// consumer rejects the file.
|
|
1240
|
-
//
|
|
1241
|
-
// An index PAST the declared layout is not a slot at all, so it cannot
|
|
1242
|
-
// justify growing the record and stays dropped — as does any override on a
|
|
1243
|
-
// class neither schema source knows, where there is no arity to grow to.
|
|
1244
|
-
let needsPad = named.some(([index]) => index >= args.length);
|
|
1245
|
-
if (!needsPad && positionalOverrides) {
|
|
1246
|
-
for (const [index] of positionalOverrides) {
|
|
1247
|
-
if (index >= args.length && index < attrNames.length) {
|
|
1248
|
-
needsPad = true;
|
|
1249
|
-
break;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
if (needsPad) {
|
|
1254
|
-
while (args.length < attrNames.length)
|
|
1255
|
-
args.push('$');
|
|
1256
|
-
}
|
|
1257
|
-
// Every `named` index is < attrNames.length by construction, and padding
|
|
1258
|
-
// has taken args.length to at least that, so each one lands.
|
|
1259
|
-
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1260
|
-
for (const [index, value] of named) {
|
|
1261
|
-
const serialized = this.serializeNamedAttribute(entityType, index, value, args[index], realSlots);
|
|
1262
|
-
// Overlay-created entities take the same rejection: a non-numeric REAL is
|
|
1263
|
-
// invalid STEP whoever authored the record. The slot keeps the `$` this
|
|
1264
|
-
// path padded it with, rather than gaining a quoted string.
|
|
1265
|
-
if (serialized === null) {
|
|
1266
|
-
onRejected?.(attrNames[index] ?? `#${index}`, value);
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
args[index] = serialized;
|
|
1270
|
-
}
|
|
1271
|
-
if (positionalOverrides && positionalOverrides.size > 0) {
|
|
1272
|
-
for (const [index, value] of positionalOverrides) {
|
|
1273
|
-
if (index < 0 || index >= args.length)
|
|
1274
|
-
continue;
|
|
1275
|
-
args[index] = this.serializePositionalOverride(entityType, index, value, args[index], realSlots, schemaVersion);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
return args.join(',');
|
|
1279
|
-
}
|
|
1280
|
-
/**
|
|
1281
|
-
* Apply positional STEP argument overrides to an entity line.
|
|
1282
|
-
* Used for non-IfcRoot edits (e.g. profile dimensions) where attributes
|
|
1283
|
-
* have no symbolic names. Indexes that fall outside the existing arg list
|
|
1284
|
-
* are silently ignored.
|
|
1285
|
-
*/
|
|
1286
|
-
applyPositionalMutations(entityText, positionals, entityType, schemaVersion) {
|
|
1287
|
-
const openParen = entityText.indexOf('(');
|
|
1288
|
-
const closeParen = entityText.lastIndexOf(');');
|
|
1289
|
-
if (openParen < 0 || closeParen < openParen)
|
|
1290
|
-
return entityText;
|
|
1291
|
-
const args = splitTopLevelArgs(entityText.slice(openParen + 1, closeParen));
|
|
1292
|
-
const realSlots = getRealTypedSlots(entityType, schemaVersion);
|
|
1293
|
-
let changed = false;
|
|
1294
|
-
for (const [index, value] of positionals) {
|
|
1295
|
-
if (index < 0 || index >= args.length)
|
|
1296
|
-
continue;
|
|
1297
|
-
args[index] = this.serializePositionalOverride(entityType, index, value, args[index], realSlots, schemaVersion);
|
|
1298
|
-
changed = true;
|
|
1299
|
-
}
|
|
1300
|
-
if (!changed)
|
|
1301
|
-
return entityText;
|
|
1302
|
-
return `${entityText.slice(0, openParen + 1)}${args.join(',')}${entityText.slice(closeParen)}`;
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Serialize one positional override, composing the schema-aware passes:
|
|
1306
|
-
* explicit `{ real }`/`{ typed }` marker → SELECT auto-qualification
|
|
1307
|
-
* (`IFCBOOLEAN(.T.)`) → REAL forcing. For REAL forcing the current source
|
|
1308
|
-
* token is a secondary signal: replacing a value that was already a REAL
|
|
1309
|
-
* (`0.4`, `1.5E-7`) keeps it REAL even for entities the XSD index doesn't
|
|
1310
|
-
* cover, so a whole-number edit can't silently downgrade the slot.
|
|
1311
|
-
*/
|
|
1312
|
-
serializePositionalOverride(entityType, index, value, currentToken, realSlots, schemaVersion) {
|
|
1313
|
-
if (isTypedMarker(value))
|
|
1314
|
-
return serializeStepValue(value);
|
|
1315
|
-
const qualified = serializeQualifiedSelectSlot(entityType, index, value);
|
|
1316
|
-
if (qualified !== null)
|
|
1317
|
-
return qualified;
|
|
1318
|
-
const forceReal = realSlots.has(index) || tokenIsRealLiteral(currentToken);
|
|
1319
|
-
return serializeStepValue(value, forceReal);
|
|
1320
|
-
}
|
|
1321
195
|
/**
|
|
1322
196
|
* Find the maximum EXPRESS ID in the data store
|
|
1323
197
|
*/
|
|
@@ -1331,7 +205,7 @@ export class StepExporter {
|
|
|
1331
205
|
*
|
|
1332
206
|
* `allocateExpressId` hands out ids from THIS exporter's `nextExpressId`,
|
|
1333
207
|
* which the property-set and quantity-set generators in
|
|
1334
|
-
* `step-property-
|
|
208
|
+
* `step-property-set-generators.ts` increment at six further sites through the same
|
|
1335
209
|
* callback — hoisting the counter onto the pass would change what it
|
|
1336
210
|
* computes, not merely where it is named, so both phases get a callback
|
|
1337
211
|
* instead (#2475 step 2a).
|
|
@@ -1345,18 +219,21 @@ export class StepExporter {
|
|
|
1345
219
|
};
|
|
1346
220
|
}
|
|
1347
221
|
/**
|
|
1348
|
-
* The state
|
|
222
|
+
* The state the property-set phase cannot read off the pass (#2475 2b/2c).
|
|
1349
223
|
*
|
|
1350
224
|
* `allocateExpressId` is the same callback `georefContext` hands out, over
|
|
1351
225
|
* the same counter, so the ids the two phases allocate stay in one sequence.
|
|
1352
226
|
* `ownerHistory` is passed by reference — the object is this exporter's, and
|
|
1353
227
|
* `export()` resets it. `isReadableSourceRef` is the instance predicate, not
|
|
1354
228
|
* `pass.isReadableSourceRef`, because two consumers of that module
|
|
1355
|
-
* (`buildRelDefinesByPropertiesIndex`, `retainSharedAtoms`
|
|
1356
|
-
* in hand; both readers are
|
|
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.
|
|
1357
232
|
*
|
|
1358
233
|
* Rebuilt per call, as `georefContext` is: every call site runs once per
|
|
1359
|
-
* export bar `retainSharedAtoms`, which hoists it out of its loop
|
|
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.
|
|
1360
237
|
*/
|
|
1361
238
|
propertySetContext() {
|
|
1362
239
|
return {
|
|
@@ -1366,85 +243,24 @@ export class StepExporter {
|
|
|
1366
243
|
isReadableSourceRef: this.isReadableSourceRef,
|
|
1367
244
|
allocateExpressId: () => this.nextExpressId++,
|
|
1368
245
|
ownerHistory: this.ownerHistory,
|
|
1369
|
-
applySourceLineMutations: (expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected) => this.
|
|
246
|
+
applySourceLineMutations: (expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected) => applySourceLineMutations(this.mutationView, expressId, entityText, recordType, attributeMutations, sourceSchema, overlayActive, onRejected),
|
|
1370
247
|
};
|
|
1371
248
|
}
|
|
1372
249
|
/**
|
|
1373
|
-
*
|
|
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.
|
|
1374
256
|
*/
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
'IFCSHAPEREPRESENTATION',
|
|
1383
|
-
'IFCPRODUCTDEFINITIONSHAPE',
|
|
1384
|
-
'IFCGEOMETRICREPRESENTATIONCONTEXT',
|
|
1385
|
-
'IFCGEOMETRICREPRESENTATIONSUBCONTEXT',
|
|
1386
|
-
'IFCEXTRUDEDAREASOLID',
|
|
1387
|
-
'IFCFACETEDBREP',
|
|
1388
|
-
'IFCPOLYLOOP',
|
|
1389
|
-
'IFCFACE',
|
|
1390
|
-
'IFCFACEOUTERBOUND',
|
|
1391
|
-
'IFCCLOSEDSHELL',
|
|
1392
|
-
'IFCRECTANGLEPROFILEDEF',
|
|
1393
|
-
'IFCCIRCLEPROFILEDEF',
|
|
1394
|
-
'IFCARBITRARYCLOSEDPROFILEDEF',
|
|
1395
|
-
'IFCPOLYLINE',
|
|
1396
|
-
'IFCTRIMMEDCURVE',
|
|
1397
|
-
'IFCBSPLINECURVE',
|
|
1398
|
-
'IFCBSPLINESURFACE',
|
|
1399
|
-
'IFCTRIANGULATEDFACESET',
|
|
1400
|
-
'IFCPOLYGONALFACE',
|
|
1401
|
-
'IFCINDEXEDPOLYGONALFACE',
|
|
1402
|
-
'IFCPOLYGONALFACESET',
|
|
1403
|
-
'IFCSTYLEDITEM',
|
|
1404
|
-
'IFCPRESENTATIONSTYLEASSIGNMENT',
|
|
1405
|
-
'IFCSURFACESTYLE',
|
|
1406
|
-
'IFCSURFACESTYLERENDERING',
|
|
1407
|
-
'IFCCOLOURRGB',
|
|
1408
|
-
]);
|
|
1409
|
-
return geometryTypes.has(type);
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Un-skip property/quantity atoms that a surviving (non-skipped, and — under
|
|
1413
|
-
* visible-only export — still-included) IfcPropertySet / IfcElementQuantity
|
|
1414
|
-
* still references.
|
|
1415
|
-
*
|
|
1416
|
-
* When a property is edited, the modified pset is replaced and its member atoms
|
|
1417
|
-
* are added to `skipIds` wholesale. Because exporters deduplicate shared
|
|
1418
|
-
* Pset_*Common atoms (e.g. a single IsExternal / IsLoadBearing value referenced
|
|
1419
|
-
* by many psets), that wholesale skip can drop an atom another pset still needs.
|
|
1420
|
-
* This pass restores any such atom: the edited pset still emits its replacement
|
|
1421
|
-
* with the new value, while the shared atom stays for the psets that keep their
|
|
1422
|
-
* original value.
|
|
1423
|
-
*/
|
|
1424
|
-
retainSharedAtoms(skipIds, allowedEntityIds) {
|
|
1425
|
-
if (skipIds.size === 0)
|
|
1426
|
-
return;
|
|
1427
|
-
// Built once for the whole sweep rather than per container: the readers in
|
|
1428
|
-
// `step-property-sets.ts` take the context, and this loop calls one of them
|
|
1429
|
-
// once per IfcPropertySet / IfcElementQuantity in the file.
|
|
1430
|
-
const ctx = this.propertySetContext();
|
|
1431
|
-
const byType = this.dataStore.entityIndex.byType;
|
|
1432
|
-
const containerIds = [
|
|
1433
|
-
...(byType.get('IFCPROPERTYSET') ?? []),
|
|
1434
|
-
...(byType.get('IFCELEMENTQUANTITY') ?? []),
|
|
1435
|
-
];
|
|
1436
|
-
for (const containerId of containerIds) {
|
|
1437
|
-
// Skipped containers are being dropped/replaced — their atoms may go.
|
|
1438
|
-
if (skipIds.has(containerId))
|
|
1439
|
-
continue;
|
|
1440
|
-
// Under visible-only export a container outside the closure is not emitted,
|
|
1441
|
-
// so it cannot keep an atom alive.
|
|
1442
|
-
if (allowedEntityIds !== null && !allowedEntityIds.has(containerId))
|
|
1443
|
-
continue;
|
|
1444
|
-
for (const atomId of getPropertyIdsInSet(ctx, containerId)) {
|
|
1445
|
-
skipIds.delete(atomId);
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
257
|
+
collectionContext() {
|
|
258
|
+
return {
|
|
259
|
+
dataStore: this.dataStore,
|
|
260
|
+
mutationView: this.mutationView,
|
|
261
|
+
propertySetContext: () => this.propertySetContext(),
|
|
262
|
+
georefContext: (deltaOnly) => this.georefContext(deltaOnly),
|
|
263
|
+
};
|
|
1448
264
|
}
|
|
1449
265
|
}
|
|
1450
266
|
/**
|