@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.
Files changed (126) hide show
  1. package/dist/csv-cell.d.ts +101 -0
  2. package/dist/csv-cell.d.ts.map +1 -0
  3. package/dist/csv-cell.js +90 -0
  4. package/dist/csv-cell.js.map +1 -0
  5. package/dist/declared-property-type.d.ts +19 -8
  6. package/dist/declared-property-type.d.ts.map +1 -1
  7. package/dist/declared-property-type.js +31 -9
  8. package/dist/declared-property-type.js.map +1 -1
  9. package/dist/demesh-session.d.ts.map +1 -1
  10. package/dist/demesh-session.js +2 -1
  11. package/dist/demesh-session.js.map +1 -1
  12. package/dist/effective-index.js +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +9 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/lod1-generator.d.ts +0 -2
  18. package/dist/lod1-generator.d.ts.map +1 -1
  19. package/dist/lod1-generator.js +1 -1
  20. package/dist/lod1-generator.js.map +1 -1
  21. package/dist/merged-exporter.d.ts +19 -0
  22. package/dist/merged-exporter.d.ts.map +1 -1
  23. package/dist/merged-exporter.js +23 -41
  24. package/dist/merged-exporter.js.map +1 -1
  25. package/dist/parquet-exporter.d.ts +1 -3
  26. package/dist/parquet-exporter.d.ts.map +1 -1
  27. package/dist/parquet-exporter.js +1 -0
  28. package/dist/parquet-exporter.js.map +1 -1
  29. package/dist/property-value-serialization.d.ts +54 -0
  30. package/dist/property-value-serialization.d.ts.map +1 -0
  31. package/dist/property-value-serialization.js +134 -0
  32. package/dist/property-value-serialization.js.map +1 -0
  33. package/dist/reference-collector.d.ts +20 -0
  34. package/dist/reference-collector.d.ts.map +1 -1
  35. package/dist/reference-collector.js +46 -113
  36. package/dist/reference-collector.js.map +1 -1
  37. package/dist/schema-converter.d.ts.map +1 -1
  38. package/dist/schema-converter.js +46 -96
  39. package/dist/schema-converter.js.map +1 -1
  40. package/dist/source-ref-bounds.d.ts +3 -3
  41. package/dist/source-ref-bounds.js +3 -3
  42. package/dist/step-attribute-mutations.d.ts +49 -0
  43. package/dist/step-attribute-mutations.d.ts.map +1 -0
  44. package/dist/step-attribute-mutations.js +172 -0
  45. package/dist/step-attribute-mutations.js.map +1 -0
  46. package/dist/step-attribute-serializers.d.ts +41 -0
  47. package/dist/step-attribute-serializers.d.ts.map +1 -0
  48. package/dist/step-attribute-serializers.js +74 -0
  49. package/dist/step-attribute-serializers.js.map +1 -0
  50. package/dist/step-collection.d.ts +55 -0
  51. package/dist/step-collection.d.ts.map +1 -0
  52. package/dist/step-collection.js +126 -0
  53. package/dist/step-collection.js.map +1 -0
  54. package/dist/step-export-contexts.d.ts +61 -0
  55. package/dist/step-export-contexts.d.ts.map +1 -0
  56. package/dist/step-export-contexts.js +50 -0
  57. package/dist/step-export-contexts.js.map +1 -0
  58. package/dist/step-export-types.d.ts +262 -0
  59. package/dist/step-export-types.d.ts.map +1 -0
  60. package/dist/step-export-types.js +22 -0
  61. package/dist/step-export-types.js.map +1 -0
  62. package/dist/step-exporter.d.ts +23 -354
  63. package/dist/step-exporter.d.ts.map +1 -1
  64. package/dist/step-exporter.js +76 -1260
  65. package/dist/step-exporter.js.map +1 -1
  66. package/dist/step-geometry-types.d.ts +3 -0
  67. package/dist/step-geometry-types.d.ts.map +1 -0
  68. package/dist/step-geometry-types.js +73 -0
  69. package/dist/step-geometry-types.js.map +1 -0
  70. package/dist/step-georeferencing.d.ts +0 -10
  71. package/dist/step-georeferencing.d.ts.map +1 -1
  72. package/dist/step-georeferencing.js +46 -94
  73. package/dist/step-georeferencing.js.map +1 -1
  74. package/dist/step-header.d.ts +53 -0
  75. package/dist/step-header.d.ts.map +1 -0
  76. package/dist/step-header.js +63 -0
  77. package/dist/step-header.js.map +1 -0
  78. package/dist/step-map-unit.d.ts +90 -0
  79. package/dist/step-map-unit.d.ts.map +1 -0
  80. package/dist/step-map-unit.js +277 -0
  81. package/dist/step-map-unit.js.map +1 -0
  82. package/dist/step-omission-predicates.d.ts +51 -0
  83. package/dist/step-omission-predicates.d.ts.map +1 -0
  84. package/dist/step-omission-predicates.js +268 -0
  85. package/dist/step-omission-predicates.js.map +1 -0
  86. package/dist/step-overlay-attribute-overrides.d.ts +32 -0
  87. package/dist/step-overlay-attribute-overrides.d.ts.map +1 -0
  88. package/dist/step-overlay-attribute-overrides.js +83 -0
  89. package/dist/step-overlay-attribute-overrides.js.map +1 -0
  90. package/dist/step-overlay-entities.d.ts +64 -0
  91. package/dist/step-overlay-entities.d.ts.map +1 -0
  92. package/dist/step-overlay-entities.js +119 -0
  93. package/dist/step-overlay-entities.js.map +1 -0
  94. package/dist/step-pass-builder.d.ts +45 -0
  95. package/dist/step-pass-builder.d.ts.map +1 -0
  96. package/dist/step-pass-builder.js +249 -0
  97. package/dist/step-pass-builder.js.map +1 -0
  98. package/dist/step-property-set-collection.d.ts +24 -0
  99. package/dist/step-property-set-collection.d.ts.map +1 -0
  100. package/dist/step-property-set-collection.js +286 -0
  101. package/dist/step-property-set-collection.js.map +1 -0
  102. package/dist/step-property-set-generators.d.ts +20 -0
  103. package/dist/step-property-set-generators.d.ts.map +1 -0
  104. package/dist/step-property-set-generators.js +98 -0
  105. package/dist/step-property-set-generators.js.map +1 -0
  106. package/dist/step-property-set-index.d.ts +25 -0
  107. package/dist/step-property-set-index.d.ts.map +1 -0
  108. package/dist/step-property-set-index.js +69 -0
  109. package/dist/step-property-set-index.js.map +1 -0
  110. package/dist/step-property-set-readers.d.ts +158 -0
  111. package/dist/step-property-set-readers.d.ts.map +1 -0
  112. package/dist/step-property-set-readers.js +251 -0
  113. package/dist/step-property-set-readers.js.map +1 -0
  114. package/dist/step-property-sets.d.ts +2 -184
  115. package/dist/step-property-sets.d.ts.map +1 -1
  116. package/dist/step-property-sets.js +22 -674
  117. package/dist/step-property-sets.js.map +1 -1
  118. package/dist/step-serialization.d.ts +33 -52
  119. package/dist/step-serialization.d.ts.map +1 -1
  120. package/dist/step-serialization.js +69 -131
  121. package/dist/step-serialization.js.map +1 -1
  122. package/dist/step-source-iteration.d.ts +97 -0
  123. package/dist/step-source-iteration.d.ts.map +1 -0
  124. package/dist/step-source-iteration.js +178 -0
  125. package/dist/step-source-iteration.js.map +1 -0
  126. package/package.json +7 -7
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builds the `ExportPass` one `export()` call threads through its phases.
3
+ *
4
+ * Split out of `step-exporter.ts` for #2475. The literal moved verbatim; the
5
+ * only edits are that the three things it used to read off the exporter
6
+ * (`dataStore`, `mutationView`, `isGeometryEntity`) now arrive as fields of
7
+ * {@link PassBuildInput}.
8
+ *
9
+ * ONE RULE GOVERNS THIS FILE, and it is easy to break by tidying. The
10
+ * predicates below are closures over `pass` ITSELF, and `pass.allowedEntityIds`
11
+ * / `pass.hiddenProductIds` are still `null` when this function returns --
12
+ * `collectModifications` assigns them afterwards, on this very object. So this
13
+ * must `return pass` and nothing else. A spread, a `structuredClone`, an
14
+ * `Object.freeze`, or a caller that stores a copy detaches every predicate from
15
+ * the object that later gets written, and each one silently answers from a
16
+ * snapshot that never fills in. That is #2637, and the failure mode is a
17
+ * `visibleOnly` export shipping a structurally wrong file rather than throwing.
18
+ */
19
+ import type { IfcDataStore, IfcSourceHeader } from '@ifc-lite/parser';
20
+ import type { MutablePropertyView } from '@ifc-lite/mutations';
21
+ import type { IfcSchemaVersion } from './schema-converter.js';
22
+ import type { ExportPass, StepExportOptions } from './step-export-types.js';
23
+ /**
24
+ * Everything the pass literal reads that is not its own field.
25
+ *
26
+ * `dataStore` and `mutationView` are the exporter's, injected rather than
27
+ * reached for. `isGeometryEntity` arrives as a parameter for a different
28
+ * reason: it is the free function in `step-geometry-types.ts`, so taking it
29
+ * here keeps this builder independent of the exporter AND of that module.
30
+ */
31
+ export interface PassBuildInput {
32
+ readonly dataStore: IfcDataStore;
33
+ readonly mutationView: MutablePropertyView | null;
34
+ readonly isGeometryEntity: (type: string) => boolean;
35
+ readonly options: StepExportOptions;
36
+ readonly schema: IfcSchemaVersion;
37
+ readonly sourceSchema: IfcSchemaVersion;
38
+ readonly converting: boolean;
39
+ readonly applyMutations: boolean;
40
+ readonly excludeGeometry: boolean;
41
+ readonly sourceHeader: IfcSourceHeader | undefined;
42
+ readonly schemaToken: string;
43
+ }
44
+ export declare function buildExportPass(input: PassBuildInput): ExportPass;
45
+ //# sourceMappingURL=step-pass-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-pass-builder.d.ts","sourceRoot":"","sources":["../src/step-pass-builder.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAqB,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAM5E;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAClD,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,eAAe,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,UAAU,CA2QjE"}
@@ -0,0 +1,249 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ import { getEffectiveEntityIndex } from './effective-index.js';
5
+ import { createModificationLedger } from './delta-modification-ledger.js';
6
+ import { createSourceRefReader } from './source-ref-bounds.js';
7
+ import { buildStepHeader } from './step-header.js';
8
+ export function buildExportPass(input) {
9
+ const { dataStore, mutationView, isGeometryEntity, options, schema, sourceSchema, converting, applyMutations, excludeGeometry, sourceHeader, schemaToken, } = input;
10
+ const pass = {
11
+ entities: [],
12
+ newEntityCount: 0,
13
+ schema,
14
+ sourceSchema,
15
+ converting,
16
+ sourceHeader,
17
+ schemaToken,
18
+ overlayActive: !!mutationView && applyMutations,
19
+ // Built once entity counts are known, so the provenance item can report the
20
+ // actual modification count. See the two call sites (empty delta + final).
21
+ // Body lives in `step-header.ts` (#2475 header/assembly tail): the
22
+ // closure still has to be built here, because it closes over this
23
+ // call's own `options`/`sourceHeader`/`schemaToken`, and both call
24
+ // sites still read it as `pass.buildHeader`.
25
+ buildHeader: (modifications) => buildStepHeader(options, sourceHeader, schemaToken, modifications),
26
+ // The one authority for exists / class / deleted, overlay first and source
27
+ // buffer second. Every pass below asks this instead of `dataStore`,
28
+ // which answers only for the file as parsed (#2012).
29
+ effective: getEffectiveEntityIndex(dataStore, mutationView, applyMutations),
30
+ // Does this id belong to an entity the OVERLAY created (`createEntity` /
31
+ // `store.addEntity`) rather than to a record in the source buffer? Such an
32
+ // entity has no source bytes, so the source-iteration pass below never sees
33
+ // it and the new-entities pass at the end owns its line entirely (#2006).
34
+ isOverlayCreated: (entityId) => pass.effective.isOverlayCreated(entityId),
35
+ // Does this record describe a line this export can actually READ out of the
36
+ // source? One predicate for every byte-range gate below, so they cannot
37
+ // disagree — see `source-ref-bounds.ts` for the corrupt file the weaker
38
+ // "is there a source / does the ref claim bytes" pair let through (#2491).
39
+ isReadableSourceRef: createSourceRefReader(dataStore.source),
40
+ // Build visible-only closure if requested. Classification, the closure walk
41
+ // and the style pass all run over the EFFECTIVE index: an overlay-created
42
+ // product becomes a root by the same type rules as a parsed one, the walk
43
+ // follows its authored references into the geometry it alone owns, and a
44
+ // tombstoned entity is simply not there. Run over the source buffer, a
45
+ // created wall could never be a root and nothing referenced it, so
46
+ // `visibleOnly` wrote a file without it and said nothing (#2012).
47
+ //
48
+ // Computed here, ahead of the modification-count passes below, because
49
+ // `hasEmittableHostBytes` needs it: a source-backed host EXCLUDED by
50
+ // `visibleOnly` never gets its line written by the source-iteration pass
51
+ // either, so counting it as "modified" would make the header claim a
52
+ // change the DATA section does not contain (CodeRabbit finding on #2414).
53
+ allowedEntityIds: null,
54
+ // Populated alongside `allowedEntityIds` below. `getVisibleEntityIds`
55
+ // excludes a hidden PRODUCT's own line from the closure, but `IFCREL*` is
56
+ // an unconditional root a few lines down and its bytes are copied verbatim
57
+ // by the source-iteration pass — nothing there filters a `#N` the closure
58
+ // just excluded out of the relationship's own attribute list. Kept because
59
+ // the closure walk's `isRefExcludedDuringClosureWalk` needs a notion of
60
+ // "hidden" that does not read `allowedEntityIds` — the set that walk is
61
+ // producing (#2398). The two OUTPUT passes no longer read this directly:
62
+ // they filter on `isOmittedFromOutput`, which subsumes it via
63
+ // `allowedEntityIds`.
64
+ hiddenProductIds: null,
65
+ // A relationship can name an excluded entity two ways that have nothing
66
+ // to do with each other: a `visibleOnly` hidden PRODUCT (`hiddenProductIds`,
67
+ // below), and a TOMBSTONED one — `editor.removeEntity` on a related object
68
+ // named by a relationship the deletion sweep below does not reach (that
69
+ // sweep only withholds an `IfcRelDefinesByProperties` when EVERY related
70
+ // object is gone, and only for that one relationship class). Left alone, a
71
+ // relationship still naming a deleted entity ships the identical `#N` with
72
+ // no `#N=` line, on a path with no `visibleOnly` involved at all (#2398).
73
+ // `effective.isDeleted` answers for every id, not just a precomputed set,
74
+ // so this predicate covers both sources without a second exclusion set.
75
+ //
76
+ // Declared here, ahead of the closure walk below, and passed into
77
+ // `collectReferencedEntityIds` as its `isRefExcluded` — rather than the
78
+ // walk inventing its own `!entityIndex.has` proxy for "deleted" that could
79
+ // disagree on an id that never existed in the file at all
80
+ // (maintainer-found regression on #2637: such an id blocked the bridge but
81
+ // did not stop the relationship's own line from shipping, dropping a
82
+ // VISIBLE sibling's pset while adding a fresh dangling ref). A closure over
83
+ // `pass.hiddenProductIds`, not a value snapshot — correct because nothing
84
+ // reads it before the closure walk assigns it just below.
85
+ //
86
+ // ## Why this is NOT the predicate the OUTPUT-line filter uses
87
+ //
88
+ // The name says walk, and only walk. The two passes that write a
89
+ // relationship's line ask `isOmittedFromOutput` (further below, derived
90
+ // from `pass.willBeEmitted`), which is strictly stronger — it also answers
91
+ // for the closure, for an unreadable source ref and for a geometry
92
+ // exclusion.
93
+ //
94
+ // This one CANNOT be `willBeEmitted`, and the difference is structural
95
+ // rather than stylistic: `willBeEmitted`'s first act is to consult
96
+ // `allowedEntityIds`, and `allowedEntityIds` is precisely what the call
97
+ // below is computing. Wiring it in here is circular: it would answer "not
98
+ // in the closure" as `false` while the closure is still being built and
99
+ // `true` for the same id afterwards.
100
+ //
101
+ // That is a genuine departure from the contract #2637 was closed on —
102
+ // `reference-collector.ts` still documents the bridge as taking the
103
+ // caller's OWN output predicate, "not two expressions that happened to
104
+ // agree". It has an OBSERVABLE consequence, not just a naming one: for an
105
+ // unreadable source ref this admits, the walk bridges through a
106
+ // relationship the output then withholds, leaving the relationship's other
107
+ // target in the closure with nothing naming it — an orphan, pinned by
108
+ // `unreadable-ref-dangling.test.ts` ("walk and output predicates diverge").
109
+ // The reverse direction is closed: every id this excludes,
110
+ // `isOmittedFromOutput` excludes too, so the #2548 leak cannot return.
111
+ isRefExcludedDuringClosureWalk: (id) => (pass.hiddenProductIds !== null && pass.hiddenProductIds.has(id))
112
+ || pass.effective.isDeleted(id),
113
+ // Will THIS entity's own line ever land in the file? The same byte-range
114
+ // test `willBeEmitted` uses (defined further below) and the source-
115
+ // iteration pass's own skip at `entityRef.byteLength === 0` — a source
116
+ // entity with no bytes (a point-cloud / GLB "entity" from
117
+ // `createSyntheticDataStore`, not an overlay-created one) never gets a
118
+ // defining line written, source-iteration or otherwise, so a pset/attribute
119
+ // edit against it must not count as a modification either: the header
120
+ // would describe a change the file does not contain (out-of-scope finding
121
+ // in #2398). Also excludes a source-backed host the visible-only closure
122
+ // above drops — same reasoning, different reason the line never lands.
123
+ //
124
+ // And, like `willBeEmitted` below, excludes a geometry-classified SOURCE
125
+ // host under `includeGeometry: false`: the source-iteration pass's own
126
+ // `isGeometryEntity` skip (further below) drops that line too, so this
127
+ // predicate must agree or a geometry entity's attribute edit inflates the
128
+ // count over an omitted line (CodeRabbit finding on #2414). Guarded by
129
+ // `!deltaOnly` for the same reason `willBeEmitted` is: under `deltaOnly`
130
+ // the source-iteration pass — and its geometry skip — never runs at all,
131
+ // so a source entity's line is assumed to already exist in the file being
132
+ // patched, geometry or not.
133
+ isGeometryExcluded: (entityId, recordType) => excludeGeometry
134
+ && isGeometryEntity(pass.effective.effectiveType(entityId, recordType)),
135
+ hasEmittableHostBytes: (entityId) => {
136
+ if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(entityId))
137
+ return false;
138
+ const ref = pass.effective.get(entityId);
139
+ // The ref must be READABLE, not merely non-empty: a range this source
140
+ // cannot address decodes to the empty string, which used to be pushed
141
+ // into the file as a blank line while everything generated FOR the host
142
+ // still named it (#2491).
143
+ if (!ref || !pass.isReadableSourceRef(ref))
144
+ return false;
145
+ if (options.deltaOnly !== true && pass.isGeometryExcluded(entityId, ref.type))
146
+ return false;
147
+ return true;
148
+ },
149
+ /**
150
+ * Will this id have a defining STEP line in the output at all?
151
+ *
152
+ * The predicate is #2030's, and it is the right one: the pset, quantity and
153
+ * type-owned passes below are built from unfiltered mutation history, and
154
+ * what each of them needs to know before emitting an
155
+ * `IFCRELDEFINESBYPROPERTIES` is not "was this deleted" or "is this hidden"
156
+ * but the general question those are two answers to. A relation naming an
157
+ * expressId that never gets written is a dangling reference and an invalid
158
+ * file, whichever route dropped the line.
159
+ *
160
+ * #2030 had to reach for four things to answer it — a tombstone probe, a
161
+ * visibility set, a byte-range test on `completeIndex`, and a `getNewEntity`
162
+ * fallback whose stated purpose was that `deleteEntity` FORGOT an
163
+ * overlay-created entity instead of tombstoning it, so `isDeleted` could not
164
+ * answer for one. That fallback was documented on main as a workaround for
165
+ * exactly the model-level defect this branch fixes: `deleteEntity` now
166
+ * tombstones as well as forgets, so the effective index answers existence
167
+ * for source and overlay ids alike and the workaround collapses into it.
168
+ *
169
+ * The overlay branch does NOT disappear with it, and the distinction matters:
170
+ * `isOverlayCreated` is still load-bearing here, because a live
171
+ * overlay-created entity has no source bytes and would fail the byte-range
172
+ * test that a source record passes. What the tombstone fix removed is the
173
+ * need for that branch to double as a deletion detector.
174
+ *
175
+ * Deliberately unchanged from #2030 for source records under `deltaOnly` /
176
+ * `exportPropertiesOnly`: the source-iteration pass is skipped wholesale in
177
+ * those modes, yet a source entity still answers true here. A delta is a
178
+ * patch against a file that already has the line, not a standalone model.
179
+ */
180
+ willBeEmitted: (entityId) => {
181
+ if (pass.allowedEntityIds !== null && !pass.allowedEntityIds.has(entityId))
182
+ return false;
183
+ // Undefined for a tombstoned id and for one neither the file nor the
184
+ // session ever had — a stale mutation must not conjure a relation either.
185
+ const ref = pass.effective.get(entityId);
186
+ if (!ref)
187
+ return false;
188
+ // An overlay-created record carries the placeholder byte range and is
189
+ // written by the new-entities pass; a source record needs real bytes.
190
+ if (pass.effective.isOverlayCreated(entityId)) {
191
+ // The overlay new-entities pass applies its OWN `isGeometryEntity`
192
+ // filter unconditionally — deltaOnly or not (see the comment at that
193
+ // loop, further below) — so this branch mirrors it without the
194
+ // deltaOnly carve-out the source branch gets.
195
+ return !pass.isGeometryExcluded(entityId, ref.type);
196
+ }
197
+ // Same readability test as `hasEmittableHostBytes`, and for the reason
198
+ // that predicate names: a ref this source cannot address is not a line
199
+ // this export can write, so nothing may be generated naming it (#2491).
200
+ if (!pass.isReadableSourceRef(ref))
201
+ return false;
202
+ // Mirrors `hasEmittableHostBytes`: under `deltaOnly` the source-
203
+ // iteration pass — and its geometry skip — never runs, so a source
204
+ // entity's line is assumed to already exist in the file being patched.
205
+ if (options.deltaOnly === true)
206
+ return true;
207
+ return !pass.isGeometryExcluded(entityId, ref.type);
208
+ },
209
+ // Under `deltaOnly` a nomination only becomes a count once some pass has
210
+ // actually written content that delivers THAT KIND of edit for the host —
211
+ // see `delta-modification-ledger.ts` for why the two are not the same event
212
+ // in that mode, and why the pair is (entity, kind) rather than the entity
213
+ // (#2462).
214
+ modifications: createModificationLedger(options.deltaOnly === true),
215
+ /**
216
+ * Hosts whose in-place named-attribute edits a FULL export may count, per
217
+ * kind. Filled by the collection passes below and read by the two passes
218
+ * that write a rewritten source line — see `in-place-nomination.ts` for why
219
+ * the nomination waits for the rewrite in this mode and not under
220
+ * `deltaOnly` (#2483).
221
+ */
222
+ inPlaceNominees: {
223
+ attribute: new Set(),
224
+ georeferencing: new Set(),
225
+ },
226
+ // Collect entities that need to be modified or created
227
+ modifiedEntities: new Set(),
228
+ modifiedAttributes: new Map(),
229
+ newPropertySets: [],
230
+ newQuantitySets: [],
231
+ typeOwnedPsetNamesByEntity: new Map(),
232
+ typeOwnedPsetIdsByEntity: new Map(),
233
+ rewrittenEntityIds: new Set(),
234
+ rewrittenEntityLines: new Map(),
235
+ /** HasPropertySets slot value for an OVERLAY-CREATED type object, applied
236
+ * by the new-entities pass (there is no source line to rewrite). */
237
+ overlayTypeOwnedPsets: new Map(),
238
+ // Track property set IDs and relationship IDs to skip
239
+ skipPropertySetIds: new Set(),
240
+ skipRelationshipIds: new Set(),
241
+ // Written by the georeferencing pass and read again by the final
242
+ // assembly, which is why they are pass state and not phase locals.
243
+ newGeorefLines: [],
244
+ warnings: [],
245
+ };
246
+ // The same object, deliberately. See the file header.
247
+ return pass;
248
+ }
249
+ //# sourceMappingURL=step-pass-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-pass-builder.js","sourceRoot":"","sources":["../src/step-pass-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { type PropertySetContext } from './step-property-set-readers.js';
2
+ import type { ExportPass, StepExportOptions } from './step-exporter.js';
3
+ /** The mutation groupings `export()` builds before the collection phase runs. */
4
+ export interface PropertyMutationGroups {
5
+ readonly entityPropMutations: Map<number, Set<string>>;
6
+ readonly entityQuantMutations: Map<number, Set<string>>;
7
+ readonly relDefinesByEntity: Map<number, Array<{
8
+ relId: number;
9
+ psetId: number;
10
+ }>>;
11
+ }
12
+ /**
13
+ * Collect what the overlay's property-set and quantity-set edits mean for this
14
+ * export: which sets to regenerate (`pass.newPropertySets` /
15
+ * `pass.newQuantitySets`), which source records to withhold
16
+ * (`pass.skipPropertySetIds` / `pass.skipRelationshipIds`), and which type
17
+ * objects need their `HasPropertySets` resolved later
18
+ * (`pass.typeOwnedPsetNamesByEntity`, `…IdsByEntity`, `pass.rewrittenEntityIds`).
19
+ *
20
+ * The caller owns the `mutationView && options.applyMutations !== false` gate;
21
+ * reaching here means overlay edits were both present and enabled.
22
+ */
23
+ export declare function collectPropertyAndQuantitySetMutations(pass: ExportPass, options: StepExportOptions, groups: PropertyMutationGroups, ctx: PropertySetContext): void;
24
+ //# sourceMappingURL=step-property-set-collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-property-set-collection.d.ts","sourceRoot":"","sources":["../src/step-property-set-collection.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,KAAK,kBAAkB,EAKxB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAExE,iFAAiF;AACjF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACvD,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CACpF;AAkBD;;;;;;;;;;GAUG;AACH,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,sBAAsB,EAC9B,GAAG,EAAE,kBAAkB,GACtB,IAAI,CAqQN"}
@@ -0,0 +1,286 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ import { extractQuantitiesOnDemand } from '@ifc-lite/parser';
5
+ import { isTypeClass } from './type-owned-psets.js';
6
+ import { getPropertySetName, getPropertyIdsInSet, getTypeOwnedHasPropertySetIds, getElementQuantityName, } from './step-property-set-readers.js';
7
+ /**
8
+ * The store the extractor THIS class installed on a view currently reads
9
+ * (#2487). The extractor is installed once per view and closes over this box
10
+ * rather than over a store directly, so a later export of the same view against
11
+ * a different store re-points it instead of answering from the first file.
12
+ *
13
+ * A box, and not a `WeakSet` of views, because ownership has to reflect the
14
+ * CURRENT state and not the historical fact that an export once installed
15
+ * something. `setQuantityExtractor` is public: a caller may install its own
16
+ * afterwards, and a marker saying "the exporter owns this view" would then keep
17
+ * overwriting a caller-supplied base forever. With a box, the second export
18
+ * writes to a box nothing reads any more and never calls the setter again, so
19
+ * the caller's extractor stands. Weak, so it never keeps a session alive.
20
+ */
21
+ const exporterQuantityBase = new WeakMap();
22
+ /**
23
+ * Collect what the overlay's property-set and quantity-set edits mean for this
24
+ * export: which sets to regenerate (`pass.newPropertySets` /
25
+ * `pass.newQuantitySets`), which source records to withhold
26
+ * (`pass.skipPropertySetIds` / `pass.skipRelationshipIds`), and which type
27
+ * objects need their `HasPropertySets` resolved later
28
+ * (`pass.typeOwnedPsetNamesByEntity`, `…IdsByEntity`, `pass.rewrittenEntityIds`).
29
+ *
30
+ * The caller owns the `mutationView && options.applyMutations !== false` gate;
31
+ * reaching here means overlay edits were both present and enabled.
32
+ */
33
+ export function collectPropertyAndQuantitySetMutations(pass, options, groups, ctx) {
34
+ const { entityPropMutations, entityQuantMutations, relDefinesByEntity } = groups;
35
+ // `export()` narrowed this through the enclosing `if`, and the caller still
36
+ // owns that gate: reaching here means the view exists and mutations are
37
+ // enabled. Named once here rather than asserted at each of the six reads.
38
+ const mutationView = ctx.mutationView;
39
+ // Collect modified property sets and find original psets to skip
40
+ for (const [entityId, psetNames] of entityPropMutations) {
41
+ // A deleted entity must not cause the exporter to REMOVE anything.
42
+ //
43
+ // This is the other half of the dangling-reference class, and the half
44
+ // `willBeEmitted` cannot reach: that predicate guards what gets ADDED,
45
+ // and this loop's real work is deciding what gets SKIPPED. An edited
46
+ // pset is replaced wholesale, so its original id goes into
47
+ // `skipPropertySetIds` — but IFC exporters share one IfcPropertySet
48
+ // between entities, and once the host is deleted there is no
49
+ // replacement to take its place. The surviving entity's relation then
50
+ // points at a container nobody wrote. Verified against main at
51
+ // e6516991 (#2030's own merge): edit `Pset_WallCommon` on one of two
52
+ // walls sharing it, delete that wall, and the export drops #11 while
53
+ // #12 still names it. `retainSharedAtoms` rescues a shared ATOM one
54
+ // level down; nothing rescues the shared container.
55
+ //
56
+ // Leaving the pset alone makes it an orphan when nothing else
57
+ // references it, which is valid IFC. Its relation is dropped by the
58
+ // sweep above, which handles a plain delete too — no pset edit needed.
59
+ if (pass.effective.isDeleted(entityId))
60
+ continue;
61
+ pass.modifiedEntities.add(entityId);
62
+ // Same rule as the attribute loop below: an overlay-CREATED entity is
63
+ // emitted once, by the new-entities pass, and already counted in
64
+ // `newEntityCount` — as are the pset entities this loop goes on to
65
+ // generate. Only the COUNT is guarded; the entity still records its
66
+ // pset edits and still emits them.
67
+ //
68
+ // A NOMINATION, in both modes, never a count on its own: this site sees
69
+ // a pset NAME the session touched, not whether that name resolves to
70
+ // anything. `deletePropertySet(id, 'AName')` on a host that owns no such
71
+ // set reaches here and changes nothing at all, and used to put "1
72
+ // modification" in the header of a byte-identical file (#2474). What
73
+ // settles it is the generator's `recordEmitted` and the skip branches'
74
+ // `recordWithheld` below.
75
+ if (!pass.isOverlayCreated(entityId) && pass.hasEmittableHostBytes(entityId)) {
76
+ pass.modifications.nominate(entityId, 'property-set');
77
+ }
78
+ // Get the FULL mutated property sets for this entity (merged base + mutations)
79
+ const allPsets = mutationView.getForEntity(entityId);
80
+ const relevantPsets = allPsets.filter((pset) => psetNames.has(pset.name));
81
+ const relDefinedPsetNames = new Set();
82
+ if (relevantPsets.length > 0) {
83
+ pass.newPropertySets.push({ entityId, psets: relevantPsets });
84
+ }
85
+ // Find original property set IDs and relationship IDs to skip — look
86
+ // up only the IfcRelDefinesByProperties rels that reference this entity.
87
+ const rels = relDefinesByEntity.get(entityId);
88
+ if (rels) {
89
+ for (const { relId, psetId: relatedPsetId } of rels) {
90
+ // Check if this pset is one we're modifying
91
+ const psetName = getPropertySetName(ctx, relatedPsetId);
92
+ if (psetName) {
93
+ relDefinedPsetNames.add(psetName);
94
+ }
95
+ if (psetName && psetNames.has(psetName)) {
96
+ pass.skipRelationshipIds.add(relId);
97
+ pass.skipPropertySetIds.add(relatedPsetId);
98
+ // Also skip the individual properties in this pset
99
+ const propIds = getPropertyIdsInSet(ctx, relatedPsetId);
100
+ for (const propId of propIds) {
101
+ pass.skipPropertySetIds.add(propId);
102
+ }
103
+ // The other half of "did this edit change the file": a full export
104
+ // applies a set DELETION by leaving these lines out, and produces
105
+ // no replacement content to record an emission for. Without this
106
+ // the count would settle from the generator alone and a real
107
+ // deletion would stop counting along with the no-op one (#2474).
108
+ pass.modifications.recordWithheld(entityId, 'property-set');
109
+ }
110
+ }
111
+ }
112
+ if (isTypeClass(pass.effective.typeOf(entityId))) {
113
+ const typeOwnedPsetIds = getTypeOwnedHasPropertySetIds(ctx, entityId, pass.effective);
114
+ const typeOwnedAffected = new Set();
115
+ for (const psetId of typeOwnedPsetIds) {
116
+ const psetName = getPropertySetName(ctx, psetId);
117
+ if (!psetName || !psetNames.has(psetName))
118
+ continue;
119
+ typeOwnedAffected.add(psetName);
120
+ pass.skipPropertySetIds.add(psetId);
121
+ const propIds = getPropertyIdsInSet(ctx, psetId);
122
+ for (const propId of propIds) {
123
+ pass.skipPropertySetIds.add(propId);
124
+ }
125
+ // No `recordWithheld` twin of the rel-defined branch above, and
126
+ // deliberately: a name that matches an OWNED pset is either dropped
127
+ // from the resolved list or swapped for the replacement this export
128
+ // generated, so slot 5 always comes back different and the repoint
129
+ // below records the emission for it. A second record here would be
130
+ // one no mutation can kill.
131
+ }
132
+ for (const psetName of psetNames) {
133
+ if (!relDefinedPsetNames.has(psetName)) {
134
+ typeOwnedAffected.add(psetName);
135
+ }
136
+ }
137
+ if (typeOwnedAffected.size > 0) {
138
+ pass.typeOwnedPsetNamesByEntity.set(entityId, typeOwnedAffected);
139
+ pass.typeOwnedPsetIdsByEntity.set(entityId, typeOwnedPsetIds);
140
+ pass.rewrittenEntityIds.add(entityId);
141
+ }
142
+ }
143
+ }
144
+ // Collect modified quantity sets (only if quantities are included)
145
+ if (options.includeQuantities === false)
146
+ entityQuantMutations.clear();
147
+ // A quantity overlay with nothing under it regenerates a source quantity
148
+ // set from the edited quantity ALONE, and the skip loop below then
149
+ // withholds the source lines that held its siblings (#2487). Unlike
150
+ // properties — whose base falls back to the `baseTable` the view was
151
+ // constructed with — quantities have only the opt-in
152
+ // `setQuantityExtractor`, so the default really is an empty base, and
153
+ // four in-tree callers plus every external embedder never set it.
154
+ //
155
+ // The exporter is the one place that always holds the missing half: it
156
+ // was handed the very store the view is an overlay ON. Supplying it here
157
+ // makes the loss impossible for every caller rather than for the callers
158
+ // we happened to find, and a view that resolves its own quantities (the
159
+ // viewer, MCP, the CLI headless backend) is never overwritten.
160
+ //
161
+ // The extractor closes over ONE store, and the view outlives this export.
162
+ // So it closes over a BOX this class owns instead: a second export of the
163
+ // same view against a DIFFERENT store re-points that box rather than
164
+ // reading the first store's quantities, which is the one way "install only
165
+ // when absent" could have answered from the wrong file. The setter is
166
+ // called at most once per view, so a caller that installs its own
167
+ // extractor at any point — before the first export or after it — keeps it.
168
+ //
169
+ // `hasQuantityBase` and `setQuantityExtractor` are probed, like every other
170
+ // optional view capability this class reaches for (`peekNextExpressId`,
171
+ // `getNewEntities`, `getEntityTypeMutation`): `MutablePropertyView` is
172
+ // published API arriving from a separately versioned package, and callers
173
+ // pass partial and duck-typed views. `hasQuantityBase` is newer than
174
+ // `setQuantityExtractor`, and without it there is no way to tell an empty
175
+ // base from a caller-supplied one — so an older view falls back to the
176
+ // pre-#2487 behaviour (no base supplied) rather than risk overwriting one.
177
+ const quantityView = mutationView;
178
+ if (entityQuantMutations.size > 0 &&
179
+ typeof quantityView.setQuantityExtractor === 'function' &&
180
+ typeof quantityView.hasQuantityBase === 'function') {
181
+ const installed = exporterQuantityBase.get(quantityView);
182
+ if (installed) {
183
+ // Ours, or a caller's that replaced ours: re-pointing the box is a
184
+ // no-op in the second case, and calling the setter again is what
185
+ // would not be.
186
+ installed.store = ctx.dataStore;
187
+ }
188
+ else if (!quantityView.hasQuantityBase()) {
189
+ const box = { store: ctx.dataStore };
190
+ exporterQuantityBase.set(quantityView, box);
191
+ quantityView.setQuantityExtractor((id) => extractQuantitiesOnDemand(box.store, id));
192
+ }
193
+ }
194
+ for (const [entityId, qsetNames] of entityQuantMutations) {
195
+ // Same rule as the property loop above: a deleted entity removes nothing.
196
+ if (pass.effective.isDeleted(entityId))
197
+ continue;
198
+ pass.modifiedEntities.add(entityId);
199
+ // See the property loop above — an overlay-created entity is counted as
200
+ // new, not modified. The pset loop's own nomination no longer has to be
201
+ // excluded to avoid a double count: the ledger settles per ENTITY, so a
202
+ // host with both a pset and a qset edit counts once whatever is
203
+ // nominated. Nominating both buys the opposite — an accurate warning
204
+ // when the qset half is the half a delta cannot carry.
205
+ //
206
+ // Settled from effect like its property-set twin (#2474). The reachable
207
+ // no-op here is an UNDONE quantity-set creation whose name matches NO
208
+ // source set: `getMutations()` is append-only, so the `CREATE_QUANTITY`
209
+ // record still names the qset after `removeQuantityMutation` has taken
210
+ // it out of the overlay, and the generator below then finds nothing to
211
+ // write. The same undo against a COLLIDING name is not a no-op — it
212
+ // withholds the source set's lines — which is what the skip loop's
213
+ // `recordWithheld` below settles.
214
+ if (!pass.isOverlayCreated(entityId) && pass.hasEmittableHostBytes(entityId)) {
215
+ pass.modifications.nominate(entityId, 'quantity-set');
216
+ }
217
+ const allQsets = mutationView.getQuantitiesForEntity(entityId);
218
+ const relevantQsets = allQsets.filter((qset) => qsetNames.has(qset.name));
219
+ if (relevantQsets.length > 0) {
220
+ pass.newQuantitySets.push({ entityId, qsets: relevantQsets });
221
+ }
222
+ // The names this export is actually WRITING a replacement for. The
223
+ // affected-name set is not the same thing: it comes from the session's
224
+ // append-only mutation history, which keeps naming a quantity set after
225
+ // an undo has taken it back out of the overlay, so a Ctrl+Z used to
226
+ // withhold a source `IfcElementQuantity` that nothing regenerated.
227
+ //
228
+ // A quantity-set REMOVAL is the one case where withholding WITHOUT a
229
+ // replacement is the intent rather than the bug. It had no public
230
+ // populator when #2487 wrote that rule, so the rule read "always the
231
+ // bug"; `MutablePropertyView.deleteQuantitySet` (#2508) gives it one,
232
+ // and the deleted set is now asked for by name below. Without that, the
233
+ // panel hid a base quantity set the exported file still carried.
234
+ const regeneratedQsetNames = new Set(relevantQsets.map((qset) => qset.name));
235
+ // Skip original quantity set entities (IfcElementQuantity).
236
+ // Same per-entity index lookup as the property branch above.
237
+ const rels = relDefinesByEntity.get(entityId);
238
+ if (rels) {
239
+ for (const { relId, psetId: relatedPsetId } of rels) {
240
+ const qsetName = getElementQuantityName(ctx, relatedPsetId);
241
+ const deleted = qsetName !== null
242
+ && mutationView.isQuantitySetDeleted?.(entityId, qsetName) === true;
243
+ if (qsetName && (regeneratedQsetNames.has(qsetName) || deleted)) {
244
+ pass.skipRelationshipIds.add(relId);
245
+ pass.skipPropertySetIds.add(relatedPsetId);
246
+ const quantIds = getPropertyIdsInSet(ctx, relatedPsetId);
247
+ for (const quantId of quantIds) {
248
+ pass.skipPropertySetIds.add(quantId);
249
+ }
250
+ // The withheld half, exactly as the rel-defined property branch
251
+ // above. This loop has just decided that #`relatedPsetId`, its
252
+ // quantity atoms and the relationship that attached them do NOT
253
+ // go into the file; whether anything is generated to take their
254
+ // place is decided elsewhere, and is not this branch's to assume.
255
+ //
256
+ // It IS assumable for the pset side and not here, and the
257
+ // difference is where the two read their base from.
258
+ // `getForEntity` merges the overlay over the base pset walk, so a
259
+ // name the session touched but did not change still resolves to
260
+ // source content and is regenerated.
261
+ // `getQuantitiesForEntity` merges the overlay over
262
+ // `quantityExtractor`, which is OPT-IN: it defaults to null, and
263
+ // several in-tree callers wire the property extractor beside it
264
+ // and not it (`cli/commands/mutate.ts`, `gym.ts`,
265
+ // `generate-spaces.ts`, `export/demesh-session.ts`), as does any
266
+ // external embedder of these two published packages. With no
267
+ // extractor the base is empty and the overlay is the only source,
268
+ // so a qset the overlay no longer holds resolves to nothing.
269
+ //
270
+ // Which makes this reachable through an UNDONE quantity-set
271
+ // creation whose name COLLIDES with a source set:
272
+ // `setQuantity(id, 'Qto_WallBaseQuantities', ...)` followed by the
273
+ // `removeQuantityMutation` that mutationSlice runs on Ctrl+Z. The
274
+ // append-only history still names the qset, so this branch
275
+ // withholds the source lines; the overlay is empty again, so
276
+ // nothing is regenerated. The export drops the source quantity set
277
+ // — a real change to the file, and a data-loss bug of its own
278
+ // (#2487) — and this call is what stops the count from calling it
279
+ // nothing.
280
+ pass.modifications.recordWithheld(entityId, 'quantity-set');
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+ //# sourceMappingURL=step-property-set-collection.js.map