@ifc-lite/export 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@
6
6
  * structure unification, and infrastructure deduplication.
7
7
  */
8
8
  import type { IfcDataStore } from '@ifc-lite/parser';
9
+ import type { MutablePropertyView } from '@ifc-lite/mutations';
9
10
  /**
10
11
  * A model to be included in the merge, with its data store and metadata.
11
12
  */
@@ -16,6 +17,29 @@ export interface MergeModelInput {
16
17
  name: string;
17
18
  /** Parsed IFC data store (must have source buffer) */
18
19
  dataStore: IfcDataStore;
20
+ /**
21
+ * Length unit scale of this model — the factor that converts the model's raw
22
+ * IFC length values into base SI metres (`1.0` metres, `0.001` millimetres,
23
+ * `0.3048` feet, …). Optional: when omitted the exporter reads
24
+ * `dataStore.lengthUnitScale`, falling back to `1.0`.
25
+ *
26
+ * The merge compares each model's scale to the first model's to decide
27
+ * whether it can be folded into the unified project (same unit) or must be
28
+ * federated as its own project (different unit). See {@link MergedExporter}.
29
+ */
30
+ lengthUnitScale?: number;
31
+ /**
32
+ * Pending edits for this model — property / attribute / quantity / retype /
33
+ * positional mutations and overlay-created entities. When present and
34
+ * non-empty, {@link MergedExporter.exportAsync} bakes them into the model
35
+ * (via {@link StepExporter}) before merging, so federated export round-trips
36
+ * edits exactly like single-model export. Empty or absent views cost nothing.
37
+ *
38
+ * Only honoured by the async `exportAsync` path: baking re-parses the edited
39
+ * bytes, which needs the async parser. The synchronous {@link MergedExporter.export}
40
+ * throws if any model carries pending edits.
41
+ */
42
+ mutationView?: MutablePropertyView;
19
43
  }
20
44
  /**
21
45
  * Options for merged STEP export
@@ -38,6 +62,24 @@ export interface MergeExportOptions {
38
62
  * - 'keep-first': Keep the first model's IfcProject as the root
39
63
  */
40
64
  projectStrategy?: 'keep-first';
65
+ /**
66
+ * How to reconcile models whose length unit differs from the first model's.
67
+ *
68
+ * - `'auto'` (default): unit-aware merge. Models that share the first
69
+ * model's length unit are unified into a single `IfcProject` (spatial
70
+ * structure and infrastructure deduplicated). A model with a *different*
71
+ * length unit is federated — it keeps its own `IfcProject`,
72
+ * `IfcUnitAssignment` and representation contexts so its coordinates stay
73
+ * correctly scaled, instead of being silently reinterpreted under the
74
+ * first model's unit. The output then contains more than one `IfcProject`
75
+ * (a deliberate relaxation of the IfcSingleProjectInstance rule, flagged in
76
+ * `stats.warnings`) — the only way to preserve mixed units in one file
77
+ * without rewriting every length-valued attribute.
78
+ * - `'assume-shared'`: treat every model as sharing the first model's unit
79
+ * (the pre-1332 behaviour). Use only when the caller has already
80
+ * normalised units; mixing real units under this mode mis-scales geometry.
81
+ */
82
+ unitReconciliation?: 'auto' | 'assume-shared';
41
83
  /** Apply visibility filtering to each model before merging */
42
84
  visibleOnly?: boolean;
43
85
  /** Hidden entity IDs per model (local expressIds) */
@@ -76,20 +118,54 @@ export interface MergeExportResult {
76
118
  totalEntityCount: number;
77
119
  /** File size in bytes */
78
120
  fileSize: number;
121
+ /**
122
+ * Number of models federated as their own IfcProject because their length
123
+ * unit differed from the first model's. 0 means a single unified project.
124
+ */
125
+ federatedModelCount: number;
126
+ /**
127
+ * Human-readable advisories about the merge (empty on a clean single-unit
128
+ * merge). Notably flags when federation produced more than one IfcProject,
129
+ * which intentionally relaxes the IfcSingleProjectInstance schema rule.
130
+ */
131
+ warnings: string[];
79
132
  };
80
133
  }
81
134
  /**
82
135
  * Merges multiple IFC models into a single STEP file.
83
136
  *
84
137
  * Uses the same approach as IfcOpenShell's MergeProjects recipe, extended
85
- * with spatial hierarchy unification:
138
+ * with spatial hierarchy unification and unit-aware federation:
86
139
  * 1. First model's entities use their original IDs
87
140
  * 2. Subsequent models' IDs are offset to avoid collisions
88
- * 3. IfcProject is unified all references remapped to the first model's project
89
- * 4. Spatial structure (Site, Building, Storey) is unified by name/elevation:
90
- * matching entities are remapped to the first model's equivalents so that
91
- * products from all models end up in the same unified tree
92
- * 5. Duplicate entities and shared infrastructure (units, contexts) are skipped
141
+ * 3. A model that shares the first model's length unit is *unified*: its
142
+ * IfcProject is remapped to the first model's, spatial structure (Site,
143
+ * Building, Storey) is unified by name/elevation, and shared infrastructure
144
+ * (units, contexts) is deduplicated.
145
+ * 4. A model with a *different* length unit is *federated*: it keeps its own
146
+ * IfcProject, IfcUnitAssignment and representation contexts, so its raw
147
+ * coordinates remain correctly scaled rather than being reinterpreted under
148
+ * the first model's unit (the mis-scale bug, issue #1332).
149
+ * 5. GlobalIds are reconciled, not blindly duplicated: a non-relationship
150
+ * rooted entity that repeats a GlobalId already emitted *in the same unit
151
+ * space* is unified (references remapped to the one instance). Otherwise —
152
+ * a federated/different-unit instance, or an objectified relationship whose
153
+ * payload (RelatedObjects) may differ — it is kept and re-stamped with a
154
+ * fresh deterministic GlobalId so the file has no duplicate-GlobalId errors
155
+ * and no relationship membership is lost.
156
+ *
157
+ * Conformance trade-off: when federation triggers, the file contains more than
158
+ * one IfcProject, which intentionally relaxes the IfcSingleProjectInstance
159
+ * EXPRESS rule (SIZEOF(IfcProject) <= 1). This is the only way to keep two
160
+ * different length units in one STEP file without rewriting every length-valued
161
+ * coordinate, and it is strictly better than the previous silent mis-scale.
162
+ * `MergeExportResult.stats.warnings` flags it; pass
163
+ * `unitReconciliation: 'assume-shared'` to force a single project when units
164
+ * are already normalised.
165
+ *
166
+ * Limitation: federation only unifies a model against the *first* model's unit
167
+ * group. Two non-first models that share a unit different from the first are
168
+ * each kept as independent projects (correct, just less deduplicated).
93
169
  */
94
170
  export declare class MergedExporter {
95
171
  private models;
@@ -101,6 +177,94 @@ export declare class MergedExporter {
101
177
  * responsive during large merged exports.
102
178
  */
103
179
  exportAsync(options: MergeExportOptions): Promise<MergeExportResult>;
180
+ /**
181
+ * Bake each model's pending edits into its source bytes before merging.
182
+ *
183
+ * A model with a non-empty {@link MergeModelInput.mutationView} is run through
184
+ * {@link StepExporter} in its own source schema (mutations applied, geometry +
185
+ * quantities kept), and the resulting bytes are re-parsed into a fresh data
186
+ * store. The merge pipeline then sees the edited entities as ordinary source,
187
+ * so unit-aware federation, GlobalId reconciliation and id offsetting are
188
+ * unaffected. Models without edits pass through untouched (no export/parse cost).
189
+ */
190
+ private bakeMutatedModels;
191
+ /**
192
+ * Assemble the result stats, including any federation conformance warnings.
193
+ */
194
+ private buildStats;
195
+ /**
196
+ * Build the ifc-lite provenance header. Merged files have no single source
197
+ * header to round-trip, so we deliberately emit our own rather than picking
198
+ * one model's FILE_DESCRIPTION arbitrarily.
199
+ */
200
+ private buildHeader;
201
+ /**
202
+ * Compute the model-independent state shared by export()/exportAsync():
203
+ * per-model id offsets and the primary model's project/infra/spatial/unit info.
204
+ */
205
+ private buildMergeSetup;
206
+ /**
207
+ * Resolve a model's length unit scale (raw IFC length → metres). Prefers an
208
+ * explicit `lengthUnitScale` on the input, else the value the parser stamped
209
+ * on the data store, else metres.
210
+ */
211
+ private resolveUnitScale;
212
+ /** True when two length unit scales are equal within relative tolerance. */
213
+ private unitsCompatible;
214
+ /**
215
+ * Resolve the set of express ids to include for a model under visibility
216
+ * filtering, or `null` when no filtering is requested (include everything).
217
+ */
218
+ private computeIncludedEntityIds;
219
+ /**
220
+ * Plan how a model's entities are remapped, skipped, or re-stamped, given
221
+ * whether it shares the primary model's length unit (`compatible`).
222
+ *
223
+ * Compatible (or `assume-shared`) models are unified into the primary project:
224
+ * their IfcProject, shared infrastructure, and matching spatial structure are
225
+ * deduplicated, and a rooted entity repeating an already-emitted GlobalId is
226
+ * unified to that one instance.
227
+ *
228
+ * Incompatible (federated) models keep their own project, units, contexts and
229
+ * spatial structure so their coordinates stay correctly scaled; a rooted
230
+ * entity whose GlobalId collides with one already emitted is given a fresh
231
+ * deterministic GlobalId, since the two cannot be the same instance across
232
+ * different unit spaces.
233
+ */
234
+ private planModel;
235
+ /**
236
+ * Render one source entity into its final STEP line: apply id offset + shared
237
+ * remaps, re-stamp a federated GlobalId if needed, apply schema conversion,
238
+ * and register the emitted GlobalId so later models can reconcile against it.
239
+ * Returns `null` when schema conversion drops the entity.
240
+ */
241
+ private renderEntity;
242
+ /**
243
+ * Read the GlobalId (first quoted attribute) from an already-rendered STEP
244
+ * line. Used to register the id that was actually emitted, after any id
245
+ * remap, GlobalId re-stamp, or schema conversion. Returns null if the first
246
+ * quoted token is not a 22-char GlobalId.
247
+ */
248
+ private readLeadingGuid;
249
+ /**
250
+ * Mint a fresh, deterministic, collision-free GlobalId for an entity whose id
251
+ * collides. Seeded from the original GlobalId and the model's stable id so the
252
+ * output is reproducible and does not churn when an unrelated earlier model
253
+ * changes size; checked against both already-emitted ids and the ids minted
254
+ * so far for this model.
255
+ */
256
+ private mintUniqueGuid;
257
+ /**
258
+ * Read an entity's GlobalId (first attribute) by decoding only its head.
259
+ * Returns the 22-char id for a rooted entity, or `null` for any entity whose
260
+ * first attribute is not a GlobalId (geometry, lists, property atoms, …).
261
+ */
262
+ private extractGlobalIdFast;
263
+ /**
264
+ * Replace an entity's GlobalId (first quoted attribute) with `newGuid`.
265
+ * `newGuid` is a 22-char IFC id (no quote in its charset), so this is safe.
266
+ */
267
+ private replaceGlobalId;
104
268
  /**
105
269
  * Remap all #ID references in a STEP entity line.
106
270
  * Applies offset to all IDs, then overrides with specific remappings.
@@ -1 +1 @@
1
- {"version":3,"file":"merged-exporter.d.ts","sourceRoot":"","sources":["../src/merged-exporter.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA0BrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,YAAY,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,eAAe,CAAC,EAAE,YAAY,CAAC;IAE/B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,sBAAsB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,0DAA0D;IAC1D,wBAAwB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAE3D,yCAAyC;IACzC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;IAC/C,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,OAAO,EAAE,UAAU,CAAC;IACpB,iBAAiB;IACjB,KAAK,EAAE;QACL,8BAA8B;QAC9B,UAAU,EAAE,MAAM,CAAC;QACnB,mCAAmC;QACnC,gBAAgB,EAAE,MAAM,CAAC;QACzB,yBAAyB;QACzB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAoB;gBAEtB,MAAM,EAAE,eAAe,EAAE;IAOrC,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB;IAmKtD;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsK1E;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAsDvB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAelC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAI1B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;;;;;;;OAQG;IACH,OAAO,CAAC,oBAAoB;IA2E5B;;;;;;;;;OASG;IACH,OAAO,CAAC,0BAA0B;IA8BlC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAazB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;CA0D7B"}
1
+ {"version":3,"file":"merged-exporter.d.ts","sourceRoot":"","sources":["../src/merged-exporter.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA6H/D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,YAAY,CAAC;IACxB;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAsBD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,eAAe,CAAC,EAAE,YAAY,CAAC;IAE/B;;;;;;;;;;;;;;;;OAgBG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IAE9C,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,sBAAsB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,0DAA0D;IAC1D,wBAAwB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAE3D,yCAAyC;IACzC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;IAC/C,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,OAAO,EAAE,UAAU,CAAC;IACpB,iBAAiB;IACjB,KAAK,EAAE;QACL,8BAA8B;QAC9B,UAAU,EAAE,MAAM,CAAC;QACnB,mCAAmC;QACnC,gBAAgB,EAAE,MAAM,CAAC;QACzB,yBAAyB;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB;;;WAGG;QACH,mBAAmB,EAAE,MAAM,CAAC;QAC5B;;;;WAIG;QACH,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAoB;gBAEtB,MAAM,EAAE,eAAe,EAAE;IAOrC,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB;IA6DtD;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAqG1E;;;;;;;;;OASG;YACW,iBAAiB;IAkC/B;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAWnB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAwBvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAQxB,4EAA4E;IAC5E,OAAO,CAAC,eAAe;IAOvB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAmBhC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,SAAS;IA+EjB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAkDpB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAWvB;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAetB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAsB3B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAUvB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAsDvB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAelC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAI1B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;;;;;;;OAQG;IACH,OAAO,CAAC,oBAAoB;IA2E5B;;;;;;;;;OASG;IACH,OAAO,CAAC,0BAA0B;IA8BlC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAazB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;CA0D7B"}