@ifc-lite/export 3.1.0 → 4.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 (152) hide show
  1. package/README.md +3 -1
  2. package/dist/anonymize-export.d.ts.map +1 -1
  3. package/dist/anonymize-export.js +108 -8
  4. package/dist/anonymize-export.js.map +1 -1
  5. package/dist/anonymize-placement.js +4 -4
  6. package/dist/anonymize-scrub.d.ts +6 -0
  7. package/dist/anonymize-scrub.d.ts.map +1 -1
  8. package/dist/anonymize-scrub.js +8 -4
  9. package/dist/anonymize-scrub.js.map +1 -1
  10. package/dist/anonymize-types.d.ts +30 -7
  11. package/dist/anonymize-types.d.ts.map +1 -1
  12. package/dist/declared-property-type.d.ts +15 -0
  13. package/dist/declared-property-type.d.ts.map +1 -1
  14. package/dist/declared-property-type.js +43 -0
  15. package/dist/declared-property-type.js.map +1 -1
  16. package/dist/georef-closure.d.ts +55 -0
  17. package/dist/georef-closure.d.ts.map +1 -0
  18. package/dist/georef-closure.js +95 -0
  19. package/dist/georef-closure.js.map +1 -0
  20. package/dist/glb.d.ts.map +1 -1
  21. package/dist/glb.js +26 -2
  22. package/dist/glb.js.map +1 -1
  23. package/dist/ifc5-classification.d.ts +36 -0
  24. package/dist/ifc5-classification.d.ts.map +1 -0
  25. package/dist/ifc5-classification.js +46 -0
  26. package/dist/ifc5-classification.js.map +1 -0
  27. package/dist/ifc5-export-helpers.d.ts +32 -0
  28. package/dist/ifc5-export-helpers.d.ts.map +1 -0
  29. package/dist/ifc5-export-helpers.js +84 -0
  30. package/dist/ifc5-export-helpers.js.map +1 -0
  31. package/dist/ifc5-exporter.d.ts.map +1 -1
  32. package/dist/ifc5-exporter.js +14 -86
  33. package/dist/ifc5-exporter.js.map +1 -1
  34. package/dist/ifc5-material.d.ts +38 -0
  35. package/dist/ifc5-material.d.ts.map +1 -0
  36. package/dist/ifc5-material.js +32 -0
  37. package/dist/ifc5-material.js.map +1 -0
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/lod0-generator.d.ts +16 -0
  43. package/dist/lod0-generator.d.ts.map +1 -1
  44. package/dist/lod0-generator.js +33 -1
  45. package/dist/lod0-generator.js.map +1 -1
  46. package/dist/merged-context.d.ts +100 -0
  47. package/dist/merged-context.d.ts.map +1 -0
  48. package/dist/merged-context.js +211 -0
  49. package/dist/merged-context.js.map +1 -0
  50. package/dist/merged-empty-containers.d.ts +63 -0
  51. package/dist/merged-empty-containers.d.ts.map +1 -0
  52. package/dist/merged-empty-containers.js +354 -0
  53. package/dist/merged-empty-containers.js.map +1 -0
  54. package/dist/merged-exporter.d.ts +28 -67
  55. package/dist/merged-exporter.d.ts.map +1 -1
  56. package/dist/merged-exporter.js +134 -258
  57. package/dist/merged-exporter.js.map +1 -1
  58. package/dist/merged-guid.d.ts +48 -0
  59. package/dist/merged-guid.d.ts.map +1 -0
  60. package/dist/merged-guid.js +128 -0
  61. package/dist/merged-guid.js.map +1 -0
  62. package/dist/merged-rel-aggregates.d.ts +63 -0
  63. package/dist/merged-rel-aggregates.d.ts.map +1 -0
  64. package/dist/merged-rel-aggregates.js +122 -0
  65. package/dist/merged-rel-aggregates.js.map +1 -0
  66. package/dist/merged-remap.d.ts +18 -0
  67. package/dist/merged-remap.d.ts.map +1 -0
  68. package/dist/merged-remap.js +66 -0
  69. package/dist/merged-remap.js.map +1 -0
  70. package/dist/merged-subcontext.d.ts +41 -0
  71. package/dist/merged-subcontext.d.ts.map +1 -0
  72. package/dist/merged-subcontext.js +99 -0
  73. package/dist/merged-subcontext.js.map +1 -0
  74. package/dist/parquet-exporter-ondemand.d.ts +34 -0
  75. package/dist/parquet-exporter-ondemand.d.ts.map +1 -0
  76. package/dist/parquet-exporter-ondemand.js +89 -0
  77. package/dist/parquet-exporter-ondemand.js.map +1 -0
  78. package/dist/parquet-exporter.d.ts +1 -29
  79. package/dist/parquet-exporter.d.ts.map +1 -1
  80. package/dist/parquet-exporter.js +59 -81
  81. package/dist/parquet-exporter.js.map +1 -1
  82. package/dist/parquet-type-strings.d.ts +11 -0
  83. package/dist/parquet-type-strings.d.ts.map +1 -0
  84. package/dist/parquet-type-strings.js +40 -0
  85. package/dist/parquet-type-strings.js.map +1 -0
  86. package/dist/parquet-uint32-columns.d.ts +30 -0
  87. package/dist/parquet-uint32-columns.d.ts.map +1 -0
  88. package/dist/parquet-uint32-columns.js +38 -0
  89. package/dist/parquet-uint32-columns.js.map +1 -0
  90. package/dist/reference-collector.d.ts +0 -31
  91. package/dist/reference-collector.d.ts.map +1 -1
  92. package/dist/reference-collector.js +27 -82
  93. package/dist/reference-collector.js.map +1 -1
  94. package/dist/related-entities.d.ts.map +1 -1
  95. package/dist/related-entities.js +19 -4
  96. package/dist/related-entities.js.map +1 -1
  97. package/dist/schema-converter-attr-remap.d.ts +53 -0
  98. package/dist/schema-converter-attr-remap.d.ts.map +1 -0
  99. package/dist/schema-converter-attr-remap.js +106 -0
  100. package/dist/schema-converter-attr-remap.js.map +1 -0
  101. package/dist/schema-converter.d.ts.map +1 -1
  102. package/dist/schema-converter.js +24 -1
  103. package/dist/schema-converter.js.map +1 -1
  104. package/dist/schema-untranslatable.d.ts +35 -0
  105. package/dist/schema-untranslatable.d.ts.map +1 -0
  106. package/dist/schema-untranslatable.js +60 -0
  107. package/dist/schema-untranslatable.js.map +1 -0
  108. package/dist/step-argument-parser.d.ts.map +1 -1
  109. package/dist/step-argument-parser.js +11 -1
  110. package/dist/step-argument-parser.js.map +1 -1
  111. package/dist/step-attribute-mutations.js +14 -11
  112. package/dist/step-attribute-mutations.js.map +1 -1
  113. package/dist/step-collection.d.ts.map +1 -1
  114. package/dist/step-collection.js +20 -6
  115. package/dist/step-collection.js.map +1 -1
  116. package/dist/step-export-types.d.ts +15 -0
  117. package/dist/step-export-types.d.ts.map +1 -1
  118. package/dist/step-export-types.js +10 -0
  119. package/dist/step-export-types.js.map +1 -1
  120. package/dist/step-exporter.d.ts.map +1 -1
  121. package/dist/step-exporter.js +9 -1
  122. package/dist/step-exporter.js.map +1 -1
  123. package/dist/step-map-unit.d.ts +2 -1
  124. package/dist/step-map-unit.d.ts.map +1 -1
  125. package/dist/step-map-unit.js.map +1 -1
  126. package/dist/step-serialization.d.ts +10 -30
  127. package/dist/step-serialization.d.ts.map +1 -1
  128. package/dist/step-serialization.js +10 -60
  129. package/dist/step-serialization.js.map +1 -1
  130. package/dist/step-source-iteration.d.ts.map +1 -1
  131. package/dist/step-source-iteration.js +21 -0
  132. package/dist/step-source-iteration.js.map +1 -1
  133. package/dist/style-closure.d.ts +91 -0
  134. package/dist/style-closure.d.ts.map +1 -0
  135. package/dist/style-closure.js +164 -0
  136. package/dist/style-closure.js.map +1 -0
  137. package/dist/subset-entity-reader.d.ts +10 -9
  138. package/dist/subset-entity-reader.d.ts.map +1 -1
  139. package/dist/subset-entity-reader.js +21 -11
  140. package/dist/subset-entity-reader.js.map +1 -1
  141. package/dist/subset-roots.d.ts +17 -1
  142. package/dist/subset-roots.d.ts.map +1 -1
  143. package/dist/subset-roots.js +67 -2
  144. package/dist/subset-roots.js.map +1 -1
  145. package/dist/type-owned-psets.d.ts +19 -0
  146. package/dist/type-owned-psets.d.ts.map +1 -1
  147. package/dist/type-owned-psets.js +31 -0
  148. package/dist/type-owned-psets.js.map +1 -1
  149. package/dist/unit-normalize.d.ts.map +1 -1
  150. package/dist/unit-normalize.js +10 -2
  151. package/dist/unit-normalize.js.map +1 -1
  152. package/package.json +8 -7
@@ -0,0 +1,211 @@
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 { asSourceBytes, STEP_TRIVIA } from '@ifc-lite/parser';
5
+ import { splitTopLevelStepArguments } from './step-argument-parser.js';
6
+ import { groupSubContextsByKey, planSubContextUnify } from './merged-subcontext.js';
7
+ /**
8
+ * `#N=TYPE(...)` record, with STEP trivia (whitespace and/or a
9
+ * `/* ... *​/` comment, #3789) tolerated between the type name and `(` —
10
+ * same adjacency fix as `entity-extractor.ts`'s `extractEntity`. Without it
11
+ * a wrapped record's attribute is silently unreadable here (`getStepAttr`
12
+ * returns `null`), which reads identically to a genuinely-absent attribute.
13
+ */
14
+ const RECORD_RE = new RegExp(`^#\\d+\\s*=\\s*\\w+${STEP_TRIVIA}\\(([\\s\\S]*)\\)\\s*;?\\s*$`);
15
+ /** Tolerance for WCS origins (metres) and normalized directions. */
16
+ const WCS_TOLERANCE_M = 1e-6;
17
+ /** 0-based attribute index of `IfcGeometricRepresentationSubContext.ParentContext`. */
18
+ const SUBCONTEXT_PARENT_CONTEXT_ATTR = 6;
19
+ function decodeEntity(dataStore, expressId) {
20
+ const source = dataStore.source;
21
+ if (!source)
22
+ return null;
23
+ const ref = dataStore.entityIndex.byId.get(expressId);
24
+ if (!ref)
25
+ return null;
26
+ return asSourceBytes(source).decodeUtf8(ref.byteOffset, ref.byteOffset + ref.byteLength);
27
+ }
28
+ /** Extract one 0-based STEP attribute of `expressId`'s entity, or null if unreadable. */
29
+ function getStepAttr(dataStore, expressId, index) {
30
+ const text = decodeEntity(dataStore, expressId);
31
+ const match = text?.match(RECORD_RE);
32
+ if (!match)
33
+ return null;
34
+ const args = splitTopLevelStepArguments(match[1]);
35
+ const raw = args?.[index];
36
+ return raw === undefined ? null : raw.trim();
37
+ }
38
+ function refId(raw) {
39
+ const m = raw?.match(/^#(\d+)$/);
40
+ return m ? parseInt(m[1], 10) : null;
41
+ }
42
+ function parseCoord(raw) {
43
+ if (raw === undefined)
44
+ return null;
45
+ const n = Number(raw.trim());
46
+ return Number.isFinite(n) ? n : null;
47
+ }
48
+ function parseVector(dataStore, directionId, fallback) {
49
+ if (directionId === null)
50
+ return fallback;
51
+ const raw = getStepAttr(dataStore, directionId, 0);
52
+ const inner = raw?.replace(/^\(|\)$/g, '');
53
+ const parts = inner === undefined ? null : splitTopLevelStepArguments(inner);
54
+ if (!parts || parts.length < 2)
55
+ return null;
56
+ const x = parseCoord(parts[0]);
57
+ const y = parseCoord(parts[1]);
58
+ const z = parts.length > 2 ? parseCoord(parts[2]) : 0;
59
+ if (x === null || y === null || z === null)
60
+ return null;
61
+ const magnitude = Math.hypot(x, y, z);
62
+ if (magnitude === 0)
63
+ return null;
64
+ return [x / magnitude, y / magnitude, z / magnitude];
65
+ }
66
+ /**
67
+ * Resolve the WorldCoordinateSystem frame of `contextId` (an
68
+ * `IfcGeometricRepresentationContext`), in metres.
69
+ *
70
+ * Attr 4 is `WorldCoordinateSystem`, an `IfcAxis2Placement` (2D/3D) whose
71
+ * attr 0 is `Location` (an `IfcCartesianPoint`), attrs 1/2 are `Axis` and
72
+ * `RefDirection` — but tolerates a context
73
+ * pointing straight at a point (no placement wrapper), the shape this
74
+ * package's own test fixtures use. Returns null when any hop is
75
+ * unresolvable (missing entity, `$`, non-numeric): callers must treat null
76
+ * permissively (compatible), never as "known to differ", so an unusual but
77
+ * harmless context shape never blocks a merge that used to work.
78
+ */
79
+ export function resolveContextWcsMetres(dataStore, contextId, lengthUnitScale) {
80
+ const wcsRef = refId(getStepAttr(dataStore, contextId, 4));
81
+ if (wcsRef === null)
82
+ return null;
83
+ const wcsType = (dataStore.entityIndex.byId.get(wcsRef)?.type ?? '').toUpperCase();
84
+ const pointId = wcsType.includes('CARTESIANPOINT') ? wcsRef : refId(getStepAttr(dataStore, wcsRef, 0));
85
+ if (pointId === null)
86
+ return null;
87
+ const coordsRaw = getStepAttr(dataStore, pointId, 0);
88
+ const inner = coordsRaw?.replace(/^\(|\)$/g, '');
89
+ const parts = inner === undefined ? null : splitTopLevelStepArguments(inner);
90
+ if (!parts || parts.length < 2)
91
+ return null;
92
+ const x = parseCoord(parts[0]);
93
+ const y = parseCoord(parts[1]);
94
+ const z = parts.length > 2 ? parseCoord(parts[2]) : 0;
95
+ if (x === null || y === null || z === null)
96
+ return null;
97
+ const scale = Number.isFinite(lengthUnitScale) && lengthUnitScale > 0 ? lengthUnitScale : 1;
98
+ // Axis2Placement defaults omitted Axis and RefDirection to +Z and +X.
99
+ // Treat omitted and explicit defaults identically, not as an unknown shape.
100
+ const isPoint = wcsType.includes('CARTESIANPOINT');
101
+ const is2dPlacement = wcsType.includes('AXIS2PLACEMENT2D');
102
+ // IFC2D places its sole RefDirection at attribute 1; treating that slot as
103
+ // a 3D Axis loses rotations in the XY frame and can wrongly unify contexts.
104
+ const axis = isPoint || is2dPlacement
105
+ ? [0, 0, 1]
106
+ : parseVector(dataStore, refId(getStepAttr(dataStore, wcsRef, 1)), [0, 0, 1]);
107
+ const refDirection = isPoint
108
+ ? [1, 0, 0]
109
+ : parseVector(dataStore, refId(getStepAttr(dataStore, wcsRef, is2dPlacement ? 1 : 2)), [1, 0, 0]);
110
+ if (axis === null || refDirection === null)
111
+ return null;
112
+ return { x: x * scale, y: y * scale, z: z * scale, axis, refDirection };
113
+ }
114
+ /** `resolveContextWcsMetres` for a model's first `IfcGeometricRepresentationContext`, or null if it has none. */
115
+ export function resolveModelContextWcs(dataStore, lengthUnitScale) {
116
+ const contextId = (dataStore.entityIndex.byType.get('IFCGEOMETRICREPRESENTATIONCONTEXT') ?? [])[0];
117
+ return contextId === undefined ? null : resolveContextWcsMetres(dataStore, contextId, lengthUnitScale);
118
+ }
119
+ /** Build the {@link PrimaryContextState} for the primary model. */
120
+ export function resolvePrimaryContextState(dataStore, subContextIds, primaryScale) {
121
+ return {
122
+ subContextsByKey: groupSubContextsByKey(dataStore, subContextIds),
123
+ contextWcs: resolveModelContextWcs(dataStore, primaryScale),
124
+ };
125
+ }
126
+ /**
127
+ * True when two resolved WCS frames are the same within tolerance, OR
128
+ * either is null (unresolvable — permissive: never block a merge on a
129
+ * context shape this reader couldn't fully parse).
130
+ */
131
+ function wcsSignaturesCompatible(a, b) {
132
+ if (a === null || b === null)
133
+ return true;
134
+ return Math.abs(a.x - b.x) <= WCS_TOLERANCE_M
135
+ && Math.abs(a.y - b.y) <= WCS_TOLERANCE_M
136
+ && Math.abs(a.z - b.z) <= WCS_TOLERANCE_M
137
+ && a.axis.every((value, index) => Math.abs(value - b.axis[index]) <= WCS_TOLERANCE_M)
138
+ && a.refDirection.every((value, index) => Math.abs(value - b.refDirection[index]) <= WCS_TOLERANCE_M);
139
+ }
140
+ /**
141
+ * Plan the whole shared-infrastructure dedup for one model — `MergedExporter`
142
+ * delegates its entire "remap and skip duplicate infrastructure" step here.
143
+ *
144
+ * `IfcUnitAssignment` unifies by position unconditionally. The representation
145
+ * context (`IFCGEOMETRICREPRESENTATIONCONTEXT`) unifies only when its resolved
146
+ * WCS frame agrees with the primary model's (origin in metres AND
147
+ * axis/refDirection orientation, so a differing length unit or a rotated frame
148
+ * doesn't produce a false match); a mismatch leaves it un-remapped — kept as
149
+ * this model's own root — so its untouched coordinates are never
150
+ * re-interpreted against the wrong frame.
151
+ *
152
+ * Its subcontexts (`IFCGEOMETRICREPRESENTATIONSUBCONTEXT`) go through
153
+ * {@link planSubContextUnify} instead of positional matching — kind-matched by
154
+ * `ContextIdentifier`/`TargetView` so a 'Body' subcontext never unifies onto an
155
+ * 'Axis' one (merge invariant lens pass 3, item 1) — gated on the SAME
156
+ * `contextsCompatible` computed once up front: a subcontext inherits its WCS
157
+ * from `ParentContext`, so when the parent root's frame disagrees, retaining
158
+ * the parent means every child must be retained too, not just remapped in
159
+ * isolation. Mutates `sharedRemap`/`skipEntityIds`.
160
+ *
161
+ * `lengthUnitScale` is the scale THIS model's raw WCS coordinates resolve
162
+ * under. Under assume-shared the caller asserts raw coordinates are already
163
+ * in the primary's unit, and they are copied verbatim — so the caller must
164
+ * pass the PRIMARY scale (matching how {@link PrimaryContextState.contextWcs}
165
+ * was computed), not the model's own declared unit, or two frames that agree
166
+ * raw-value-for-raw-value can compare unequal (and vice versa: differing raw
167
+ * origins can collide once scaled). Under auto/normalize the caller passes
168
+ * the model's own declared scale, comparing true metric frames.
169
+ */
170
+ export function planInfrastructureUnify(dataStore, modelInfra, firstModelInfraMap, firstModelContext, firstModelOffset, lengthUnitScale, sharedRemap, skipEntityIds) {
171
+ // Computed once, up front — not per type in the loop below — so its result
172
+ // does not depend on `IFCGEOMETRICREPRESENTATIONCONTEXT` iterating before
173
+ // `IFCGEOMETRICREPRESENTATIONSUBCONTEXT` in `firstModelInfraMap`.
174
+ const contextsCompatible = wcsSignaturesCompatible(firstModelContext.contextWcs, resolveModelContextWcs(dataStore, lengthUnitScale));
175
+ // Only the model's FIRST top-level context is (positionally) unified below;
176
+ // any later sibling context — a model with both a 'Model' and a 'Plan'
177
+ // context, say — is always retained. A subcontext may therefore unify ONLY
178
+ // when its own `ParentContext` is the context actually being remapped: a
179
+ // child of a retained sibling must stay with its parent, or its shape
180
+ // representations get re-anchored onto the primary's frame while the parent
181
+ // keeps its own.
182
+ const modelContextIds = modelInfra.get('IFCGEOMETRICREPRESENTATIONCONTEXT') ?? [];
183
+ const firstContextIds = firstModelInfraMap.get('IFCGEOMETRICREPRESENTATIONCONTEXT') ?? [];
184
+ const unifiedContextId = contextsCompatible && modelContextIds.length > 0 && firstContextIds.length > 0
185
+ ? modelContextIds[0]
186
+ : null;
187
+ for (const [type, firstIds] of firstModelInfraMap) {
188
+ const thisIds = modelInfra.get(type);
189
+ if (!thisIds || firstIds.length === 0 || thisIds.length === 0)
190
+ continue;
191
+ if (type === 'IFCGEOMETRICREPRESENTATIONSUBCONTEXT') {
192
+ if (unifiedContextId === null)
193
+ continue;
194
+ // An unreadable ParentContext stays permissive (treated as a child of
195
+ // the unified context), matching this file's null-handling doctrine.
196
+ const childIds = thisIds.filter((id) => {
197
+ const parentId = refId(getStepAttr(dataStore, id, SUBCONTEXT_PARENT_CONTEXT_ATTR));
198
+ return parentId === null || parentId === unifiedContextId;
199
+ });
200
+ if (childIds.length > 0) {
201
+ planSubContextUnify(dataStore, childIds, firstModelContext.subContextsByKey, firstModelOffset, sharedRemap, skipEntityIds);
202
+ }
203
+ continue;
204
+ }
205
+ if (type === 'IFCGEOMETRICREPRESENTATIONCONTEXT' && !contextsCompatible)
206
+ continue;
207
+ sharedRemap.set(thisIds[0], firstIds[0] + firstModelOffset);
208
+ skipEntityIds.add(thisIds[0]);
209
+ }
210
+ }
211
+ //# sourceMappingURL=merged-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-context.js","sourceRoot":"","sources":["../src/merged-context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Empty-spatial-container analysis for the merged exporter (issue #3643) — the
3
+ * step of IfcOpenShell/BlenderBIM's "Merge Projects" recipe that container
4
+ * *matching* (`mergeSites` / `mergeBuildings` / `mergeStoreys`) leaves behind.
5
+ *
6
+ * An `IfcSite` / `IfcBuilding` / `IfcBuildingStorey` / `IfcSpace` is empty when
7
+ * it contains no surviving element (`IfcRelContainedInSpatialStructure`),
8
+ * directly aggregates no surviving non-spatial object, and transitively
9
+ * aggregates no non-empty spatial child. `IfcProject` is never a candidate.
10
+ *
11
+ * Emptiness is a fact about the MERGED model, not about one input: a site that
12
+ * is empty in its own file is not empty once a later model's storeys are unified
13
+ * onto it. So this runs across every model — after visibility filtering and
14
+ * after spatial unification, the two things that make a container empty in the
15
+ * first place — and yields the express ids the emit loop then never writes.
16
+ * Nothing is emitted referencing them, so no dangling-reference clean-up pass
17
+ * has to follow; that is why this belongs inside the merge rather than after it.
18
+ *
19
+ * The Rust twin is `rust/export/src/merged/empty.rs`, which the native merged
20
+ * export (`export_merged_models`) drives from the same rules.
21
+ */
22
+ import type { IfcSourceBytes } from '@ifc-lite/parser';
23
+ import type { CompleteEntityIndex } from './entity-iteration.js';
24
+ /** One model as the analysis sees it, in the merge's own terms. */
25
+ export interface EmptyContainerModelView {
26
+ /** Complete entity index (primary + deferred atoms). */
27
+ entities: CompleteEntityIndex;
28
+ /** The model's source bytes. */
29
+ source: IfcSourceBytes;
30
+ /** Visible express ids, or `null` when the whole model is exported. */
31
+ included: ReadonlySet<number> | null;
32
+ /** Local express id → final express id, for containers unified onto an
33
+ * earlier model's (the spatial-unification remap). */
34
+ sharedRemap: ReadonlyMap<number, number>;
35
+ /** Express-id offset applied to this model in the merged file. */
36
+ offset: number;
37
+ /** True when the model unifies into the first model's project / unit space. */
38
+ compatible: boolean;
39
+ }
40
+ /** Which containers the emit loop must not write. */
41
+ export interface EmptyContainerPlan {
42
+ /** Per input model (same order): local express ids never emitted. */
43
+ droppedByModel: Set<number>[];
44
+ /** Containers dropped, counted in the MERGED model (a container unified
45
+ * across three inputs counts once). */
46
+ droppedCount: number;
47
+ }
48
+ /**
49
+ * A model with nothing to analyse — the placeholder for an input whose source
50
+ * bytes are missing (a cache-restored, metadata-only store), which the emit loop
51
+ * skips too. Keeps the caller's per-model arrays aligned with its inputs.
52
+ */
53
+ export declare const EMPTY_MODEL_VIEW: EmptyContainerModelView;
54
+ /**
55
+ * Plan which spatial containers the merge leaves holding nothing.
56
+ *
57
+ * The returned local ids are what the caller withholds from the output; every
58
+ * surviving line that named one is narrowed (or withheld with it) by
59
+ * `filterHiddenRefsFromRelationshipLine`, which is what keeps the result free of
60
+ * dangling references.
61
+ */
62
+ export declare function planEmptyContainerDrops(models: EmptyContainerModelView[]): EmptyContainerPlan;
63
+ //# sourceMappingURL=merged-empty-containers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-empty-containers.d.ts","sourceRoot":"","sources":["../src/merged-empty-containers.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAkCjE,mEAAmE;AACnE,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gCAAgC;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACrC;2DACuD;IACvD,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qDAAqD;AACrD,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9B;4CACwC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB;AAOD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,uBAO9B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,uBAAuB,EAAE,GAAG,kBAAkB,CA2C7F"}
@@ -0,0 +1,354 @@
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 { asSourceBytes, STEP_TRIVIA } from '@ifc-lite/parser';
5
+ import { splitTopLevelArgs } from './step-argument-parser.js';
6
+ /**
7
+ * `#N=TYPE(...)` record, with STEP trivia (whitespace and/or a
8
+ * `/* ... *​/` comment, #3789) tolerated between the type name and `(` —
9
+ * same adjacency fix as `entity-extractor.ts`'s `extractEntity`. Without it
10
+ * a wrapped line's arguments are unreadable here (`topLevelAttrs` returns
11
+ * `null`), which the caller treats as "can't safely rewrite" and blocks the
12
+ * empty-container drop for that entity.
13
+ */
14
+ const RECORD_RE = new RegExp(`^#\\d+\\s*=\\s*\\w+${STEP_TRIVIA}\\(([\\s\\S]*)\\)\\s*;?\\s*$`);
15
+ /**
16
+ * Spatial container types dropped when they end up empty. `IfcProject` is
17
+ * deliberately absent: it is the file's root, never a candidate.
18
+ */
19
+ const CONTAINER_TYPES = new Set([
20
+ 'IFCSITE',
21
+ 'IFCBUILDING',
22
+ 'IFCBUILDINGSTOREY',
23
+ 'IFCSPACE',
24
+ ]);
25
+ /**
26
+ * Relationships that express spatial structure, as
27
+ * `[relatingAttributeIndex, relatedAttributeIndex]` — `IfcRelAggregates` names
28
+ * the whole first, the containment relationships name the parts first.
29
+ */
30
+ const STRUCTURE_RELATIONS = {
31
+ IFCRELAGGREGATES: [4, 5],
32
+ IFCRELCONTAINEDINSPATIALSTRUCTURE: [5, 4],
33
+ IFCRELREFERENCEDINSPATIALSTRUCTURE: [5, 4],
34
+ };
35
+ /** True when `typeUpper` is a droppable spatial container type. */
36
+ function isSpatialContainerType(typeUpper) {
37
+ return CONTAINER_TYPES.has(typeUpper);
38
+ }
39
+ /**
40
+ * A model with nothing to analyse — the placeholder for an input whose source
41
+ * bytes are missing (a cache-restored, metadata-only store), which the emit loop
42
+ * skips too. Keeps the caller's per-model arrays aligned with its inputs.
43
+ */
44
+ export const EMPTY_MODEL_VIEW = {
45
+ entities: new Map(),
46
+ source: asSourceBytes(new Uint8Array()),
47
+ included: null,
48
+ sharedRemap: new Map(),
49
+ offset: 0,
50
+ compatible: false,
51
+ };
52
+ /**
53
+ * Plan which spatial containers the merge leaves holding nothing.
54
+ *
55
+ * The returned local ids are what the caller withholds from the output; every
56
+ * surviving line that named one is narrowed (or withheld with it) by
57
+ * `filterHiddenRefsFromRelationshipLine`, which is what keeps the result free of
58
+ * dangling references.
59
+ */
60
+ export function planEmptyContainerDrops(models) {
61
+ const nodes = new Set();
62
+ const hasContent = new Set();
63
+ const blocked = new Set();
64
+ /** Spatial child node → its container nodes (upward edges for the sweep). */
65
+ const parents = new Map();
66
+ const guidNode = new Map();
67
+ const containersByModel = [];
68
+ for (const view of models) {
69
+ const canon = canonicalContainers(view, guidNode);
70
+ containersByModel.push(canon);
71
+ for (const node of canon.values())
72
+ nodes.add(node);
73
+ recordEdges(view, canon, hasContent, parents);
74
+ recordBlocks(view, canon, blocked);
75
+ }
76
+ // A container is non-empty when it holds something, is blocked, or has a
77
+ // non-empty spatial child — the last propagated upward from the first two.
78
+ // The visited check also makes a self-referential aggregation terminate.
79
+ const nonEmpty = new Set([...hasContent, ...blocked]);
80
+ const stack = [...nonEmpty];
81
+ while (stack.length > 0) {
82
+ const node = stack.pop();
83
+ for (const parent of parents.get(node) ?? []) {
84
+ if (!nonEmpty.has(parent)) {
85
+ nonEmpty.add(parent);
86
+ stack.push(parent);
87
+ }
88
+ }
89
+ }
90
+ const dropped = new Set([...nodes].filter(node => !nonEmpty.has(node)));
91
+ return {
92
+ droppedByModel: containersByModel.map(canon => {
93
+ const ids = new Set();
94
+ for (const [localId, node] of canon) {
95
+ if (dropped.has(node))
96
+ ids.add(localId);
97
+ }
98
+ return ids;
99
+ }),
100
+ droppedCount: dropped.size,
101
+ };
102
+ }
103
+ /**
104
+ * Map each of this model's visible containers to its node in the merged model:
105
+ * the container it was unified onto, the earlier instance that already carries
106
+ * its GlobalId, or its own final express id. Mirrors the emit loop's
107
+ * unification order (iteration order, first occurrence wins).
108
+ */
109
+ function canonicalContainers(view, guidNode) {
110
+ const canon = new Map();
111
+ for (const [localId, ref] of view.entities) {
112
+ if (view.included !== null && !view.included.has(localId))
113
+ continue;
114
+ if (!isSpatialContainerType(ref.type.toUpperCase()))
115
+ continue;
116
+ const unified = view.sharedRemap.get(localId);
117
+ if (unified !== undefined) {
118
+ canon.set(localId, unified);
119
+ continue;
120
+ }
121
+ const finalId = localId + view.offset;
122
+ if (!view.compatible) {
123
+ canon.set(localId, finalId);
124
+ continue;
125
+ }
126
+ const guid = leadingGuid(lineOf(view, localId));
127
+ if (guid === null) {
128
+ canon.set(localId, finalId);
129
+ continue;
130
+ }
131
+ const known = guidNode.get(guid);
132
+ if (known === undefined)
133
+ guidNode.set(guid, finalId);
134
+ canon.set(localId, known ?? finalId);
135
+ }
136
+ return canon;
137
+ }
138
+ /**
139
+ * Record what this model contributes to each container: contained elements and
140
+ * aggregated objects become content; aggregated spatial children become upward
141
+ * edges, so a non-empty storey keeps its building and its site.
142
+ */
143
+ function recordEdges(view, canon, hasContent, parents) {
144
+ if (canon.size === 0)
145
+ return;
146
+ for (const [localId, ref] of view.entities) {
147
+ if (view.included !== null && !view.included.has(localId))
148
+ continue;
149
+ const slots = STRUCTURE_RELATIONS[ref.type.toUpperCase()];
150
+ if (slots === undefined)
151
+ continue;
152
+ const attrs = topLevelAttrs(lineOf(view, localId));
153
+ if (attrs === null)
154
+ continue;
155
+ const [relatingIndex, relatedIndex] = slots;
156
+ const relating = singleRef(attrs[relatingIndex] ?? '');
157
+ // Not a container (an IfcProject aggregating its sites, say) — its children
158
+ // are roots of the spatial tree, with nothing above them to keep.
159
+ if (relating === null)
160
+ continue;
161
+ const parent = canon.get(relating);
162
+ if (parent === undefined)
163
+ continue;
164
+ for (const child of refList(attrs[relatedIndex] ?? '')) {
165
+ if (view.included !== null && !view.included.has(child))
166
+ continue;
167
+ const childNode = canon.get(child);
168
+ if (childNode === undefined) {
169
+ hasContent.add(parent);
170
+ continue;
171
+ }
172
+ const edges = parents.get(childNode);
173
+ if (edges === undefined)
174
+ parents.set(childNode, [parent]);
175
+ else
176
+ edges.push(parent);
177
+ }
178
+ }
179
+ }
180
+ /**
181
+ * Block every container this model names in a way the emit loop could not
182
+ * rewrite. Dropping such a container would leave a dangling `#ref`, so it stays
183
+ * (counting as non-empty, which keeps its ancestors too) instead.
184
+ *
185
+ * Rewritable is: a reference from an objectified relationship (`IfcRel*`) that
186
+ * nothing else references, since `filterHiddenRefsFromRelationshipLine` can
187
+ * strip it from a list or withhold the whole line. Everything else — a
188
+ * non-relationship referrer, a reference nested inside a typed value, or a
189
+ * relationship that is itself referenced (withholding it would dangle in turn) —
190
+ * blocks.
191
+ */
192
+ function recordBlocks(view, canon, blocked) {
193
+ if (canon.size === 0)
194
+ return;
195
+ const referrers = [];
196
+ const referencedRelationships = new Set();
197
+ for (const [localId, ref] of view.entities) {
198
+ if (view.included !== null && !view.included.has(localId))
199
+ continue;
200
+ let namesContainer = false;
201
+ for (const target of argRefs(view.source.slice(ref.byteOffset, ref.byteOffset + ref.byteLength))) {
202
+ if (canon.has(target))
203
+ namesContainer = true;
204
+ const targetType = view.entities.get(target)?.type.toUpperCase();
205
+ if (targetType?.startsWith('IFCREL'))
206
+ referencedRelationships.add(target);
207
+ }
208
+ if (namesContainer)
209
+ referrers.push(localId);
210
+ }
211
+ for (const localId of referrers) {
212
+ const typeUpper = view.entities.get(localId).type.toUpperCase();
213
+ const rewritable = typeUpper.startsWith('IFCREL') && !referencedRelationships.has(localId);
214
+ const line = lineOf(view, localId);
215
+ const slots = classifyRefs(line);
216
+ if (slots === null) {
217
+ // The argument list could not be parsed, so no rewrite can narrow this
218
+ // line: every container it names has to stay.
219
+ for (const target of argRefs(line)) {
220
+ const node = canon.get(target);
221
+ if (node !== undefined)
222
+ blocked.add(node);
223
+ }
224
+ continue;
225
+ }
226
+ for (const [target, nested] of slots) {
227
+ const node = canon.get(target);
228
+ if (node === undefined)
229
+ continue;
230
+ if (nested || !rewritable)
231
+ blocked.add(node);
232
+ }
233
+ }
234
+ }
235
+ /** The entity's STEP line text. */
236
+ function lineOf(view, localId) {
237
+ const ref = view.entities.get(localId);
238
+ if (ref === undefined)
239
+ return '';
240
+ return view.source.decodeUtf8(ref.byteOffset, ref.byteOffset + ref.byteLength);
241
+ }
242
+ /** Top-level arguments of a `#id=TYPE(…);` line, or `null` when unparseable. */
243
+ function topLevelAttrs(line) {
244
+ const match = line.match(RECORD_RE);
245
+ if (!match)
246
+ return null;
247
+ return splitTopLevelArgs(match[1]).map(arg => arg.trim());
248
+ }
249
+ /** Parse an argument that is exactly one reference (`"#7"` → `7`). */
250
+ function singleRef(arg) {
251
+ const match = arg.trim().match(/^#(\d+)$/);
252
+ return match ? Number(match[1]) : null;
253
+ }
254
+ /** Parse a `(#a,#b,…)` list argument into ids. */
255
+ function refList(arg) {
256
+ const trimmed = arg.trim();
257
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')'))
258
+ return [];
259
+ const inner = trimmed.slice(1, -1).trim();
260
+ if (inner === '')
261
+ return [];
262
+ const ids = [];
263
+ for (const item of splitTopLevelArgs(inner)) {
264
+ const id = singleRef(item);
265
+ if (id !== null)
266
+ ids.push(id);
267
+ }
268
+ return ids;
269
+ }
270
+ /**
271
+ * Every reference in a line's argument list, paired with whether it sits
272
+ * somewhere neither `filterHiddenRefsFromRelationshipLine` can strip (a direct
273
+ * list element) nor withhold the line for (a whole single-valued attribute) —
274
+ * i.e. nested inside a list of lists or a typed value. `null` when the line's
275
+ * argument list cannot be parsed at all: a line no rewrite can narrow, which the
276
+ * analysis must treat as a blocker rather than as "names nothing".
277
+ */
278
+ function classifyRefs(line) {
279
+ const attrs = topLevelAttrs(line);
280
+ if (attrs === null)
281
+ return null;
282
+ const out = [];
283
+ for (const attr of attrs) {
284
+ const direct = singleRef(attr);
285
+ if (direct !== null) {
286
+ out.push([direct, false]);
287
+ continue;
288
+ }
289
+ if (attr.startsWith('(') && attr.endsWith(')')) {
290
+ const inner = attr.slice(1, -1).trim();
291
+ if (inner === '')
292
+ continue;
293
+ for (const item of splitTopLevelArgs(inner)) {
294
+ const id = singleRef(item);
295
+ if (id !== null)
296
+ out.push([id, false]);
297
+ else
298
+ for (const nested of argRefs(item))
299
+ out.push([nested, true]);
300
+ }
301
+ continue;
302
+ }
303
+ for (const nested of argRefs(attr))
304
+ out.push([nested, true]);
305
+ }
306
+ return out;
307
+ }
308
+ /**
309
+ * Every `#N` reference in a chunk of STEP text, skipping quoted strings (where a
310
+ * `#` is literal) and any leading `#id=` (the line's own id). Reads bytes rather
311
+ * than decoded text so the reverse-reference scan never decodes a whole model.
312
+ */
313
+ function argRefs(text) {
314
+ const bytes = typeof text === 'string' ? new TextEncoder().encode(text) : text;
315
+ const eq = bytes.indexOf(0x3d /* = */);
316
+ const from = eq === -1 ? 0 : eq + 1;
317
+ const out = [];
318
+ let inString = false;
319
+ for (let i = from; i < bytes.length; i++) {
320
+ const byte = bytes[i];
321
+ if (byte === 0x27 /* ' */) {
322
+ inString = !inString;
323
+ continue;
324
+ }
325
+ if (inString || byte !== 0x23 /* # */)
326
+ continue;
327
+ let j = i + 1;
328
+ let value = 0;
329
+ while (j < bytes.length && bytes[j] >= 0x30 && bytes[j] <= 0x39) {
330
+ value = value * 10 + (bytes[j] - 0x30);
331
+ j++;
332
+ }
333
+ if (j > i + 1) {
334
+ out.push(value);
335
+ i = j - 1;
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+ /** The GlobalId (first quoted attribute) of a rooted entity's line. */
341
+ function leadingGuid(line) {
342
+ const open = line.indexOf('(');
343
+ if (open === -1)
344
+ return null;
345
+ const first = line.indexOf("'", open + 1);
346
+ if (first === -1)
347
+ return null;
348
+ const second = line.indexOf("'", first + 1);
349
+ if (second === -1)
350
+ return null;
351
+ const raw = line.slice(first + 1, second);
352
+ return /^[0-9A-Za-z_$]{22}$/.test(raw) ? raw : null;
353
+ }
354
+ //# sourceMappingURL=merged-empty-containers.js.map