@ifc-lite/parser 5.1.0 → 5.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.
- package/dist/classification-resolver.d.ts.map +1 -1
- package/dist/classification-resolver.js +35 -10
- package/dist/classification-resolver.js.map +1 -1
- package/dist/columnar-entity-preparation.d.ts +22 -0
- package/dist/columnar-entity-preparation.d.ts.map +1 -0
- package/dist/columnar-entity-preparation.js +192 -0
- package/dist/columnar-entity-preparation.js.map +1 -0
- package/dist/columnar-parser-root-attributes.d.ts +66 -0
- package/dist/columnar-parser-root-attributes.d.ts.map +1 -0
- package/dist/columnar-parser-root-attributes.js +245 -0
- package/dist/columnar-parser-root-attributes.js.map +1 -0
- package/dist/columnar-parser.d.ts +16 -62
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +371 -750
- package/dist/columnar-parser.js.map +1 -1
- package/dist/compact-entity-index-columns.d.ts +17 -0
- package/dist/compact-entity-index-columns.d.ts.map +1 -0
- package/dist/compact-entity-index-columns.js +15 -0
- package/dist/compact-entity-index-columns.js.map +1 -0
- package/dist/compact-entity-index-transport.d.ts +2 -12
- package/dist/compact-entity-index-transport.d.ts.map +1 -1
- package/dist/compact-entity-index-transport.js +1 -11
- package/dist/compact-entity-index-transport.js.map +1 -1
- package/dist/compact-entity-index.d.ts +6 -0
- package/dist/compact-entity-index.d.ts.map +1 -1
- package/dist/compact-entity-index.js +12 -20
- package/dist/compact-entity-index.js.map +1 -1
- package/dist/data-store-transport.d.ts +2 -2
- package/dist/data-store-transport.d.ts.map +1 -1
- package/dist/data-store-transport.js +5 -5
- package/dist/data-store-transport.js.map +1 -1
- package/dist/entity-refs-from-index.d.ts +10 -0
- package/dist/entity-refs-from-index.d.ts.map +1 -1
- package/dist/entity-refs-from-index.js +52 -36
- package/dist/entity-refs-from-index.js.map +1 -1
- package/dist/entity-scanner.d.ts +5 -0
- package/dist/entity-scanner.d.ts.map +1 -1
- package/dist/entity-scanner.js +20 -7
- package/dist/entity-scanner.js.map +1 -1
- package/dist/entity-type-byte-interner.d.ts +12 -0
- package/dist/entity-type-byte-interner.d.ts.map +1 -0
- package/dist/entity-type-byte-interner.js +66 -0
- package/dist/entity-type-byte-interner.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/parser.worker.d.ts +6 -3
- package/dist/parser.worker.d.ts.map +1 -1
- package/dist/parser.worker.js +24 -12
- package/dist/parser.worker.js.map +1 -1
- package/dist/prepass-source-fingerprint.d.ts +3 -0
- package/dist/prepass-source-fingerprint.d.ts.map +1 -0
- package/dist/prepass-source-fingerprint.js +13 -0
- package/dist/prepass-source-fingerprint.js.map +1 -0
- package/dist/select-entity-columns.d.ts +7 -0
- package/dist/select-entity-columns.d.ts.map +1 -0
- package/dist/select-entity-columns.js +43 -0
- package/dist/select-entity-columns.js.map +1 -0
- package/dist/spatial-hierarchy-builder.d.ts +6 -8
- package/dist/spatial-hierarchy-builder.d.ts.map +1 -1
- package/dist/spatial-hierarchy-builder.js +4 -0
- package/dist/spatial-hierarchy-builder.js.map +1 -1
- package/dist/spatial-hierarchy-canonical-parent.d.ts +36 -0
- package/dist/spatial-hierarchy-canonical-parent.d.ts.map +1 -0
- package/dist/spatial-hierarchy-canonical-parent.js +63 -0
- package/dist/spatial-hierarchy-canonical-parent.js.map +1 -0
- package/dist/worker-index-publication.d.ts +43 -0
- package/dist/worker-index-publication.d.ts.map +1 -0
- package/dist/worker-index-publication.js +129 -0
- package/dist/worker-index-publication.js.map +1 -0
- package/dist/worker-parser.d.ts +5 -2
- package/dist/worker-parser.d.ts.map +1 -1
- package/dist/worker-parser.js +22 -10
- package/dist/worker-parser.js.map +1 -1
- package/package.json +3 -3
package/dist/columnar-parser.js
CHANGED
|
@@ -4,562 +4,412 @@
|
|
|
4
4
|
import { SpatialHierarchyBuilder } from './spatial-hierarchy-builder.js';
|
|
5
5
|
import { EntityExtractor } from './entity-extractor.js';
|
|
6
6
|
import { extractLengthUnitScale } from './unit-extractor.js';
|
|
7
|
-
import { getAttributeNames, getAttributeNamesAcrossSchemas, getInheritanceChain } from './ifc-schema.js';
|
|
8
7
|
import { parsePropertyValueWithComplex } from './on-demand-extractors.js';
|
|
9
8
|
import { readQuantitySet } from './quantity-collect.js';
|
|
10
|
-
import {
|
|
9
|
+
import { prepareColumnarEntities } from './columnar-entity-preparation.js';
|
|
11
10
|
import { yieldToEventLoop } from './yield-to-event-loop.js';
|
|
12
11
|
import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, } from '@ifc-lite/data';
|
|
13
12
|
import { BufferEntitySource } from './entity-source.js';
|
|
14
13
|
import { batchExtractGlobalIdAndName } from './columnar-parser-attributes.js';
|
|
15
|
-
import {
|
|
14
|
+
import { REL_TYPE_MAP, } from './columnar-parser-indexes.js';
|
|
16
15
|
import { extractRelFast, extractPropertyRelFast } from './columnar-parser-relationships.js';
|
|
17
16
|
import { detectSchemaVersion, parseSourceHeader } from './source-header.js';
|
|
18
17
|
import { contiguousSourceBytes } from './source-bytes.js';
|
|
18
|
+
import { getEntityRefFromStore, extractRootAttributesFromEntity, pickLongName } from './columnar-parser-root-attributes.js';
|
|
19
|
+
// Re-exported: part of the package's public on-demand-extraction surface
|
|
20
|
+
// (see packages/parser/src/index.ts).
|
|
21
|
+
export { extractEntityAttributesOnDemand, extractAllEntityAttributes, getRawNamedAttributes, extractRootAttributesFromEntity, } from './columnar-parser-root-attributes.js';
|
|
19
22
|
/** Appends `ref` to `map.get(objId)`, skipping a repeat — these maps win over the deduped graph (#3760/#3782). */
|
|
20
23
|
function addOnDemandRef(map, objId, ref) {
|
|
21
24
|
const list = map.get(objId) ?? map.set(objId, []).get(objId);
|
|
22
25
|
if (!list.includes(ref))
|
|
23
26
|
list.push(ref);
|
|
24
27
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const deferPropertyAtomIndex = options.deferPropertyAtomIndex === true;
|
|
70
|
-
const typeUpperCache = new Map();
|
|
71
|
-
const getTypeUpper = (type) => {
|
|
72
|
-
let upper = typeUpperCache.get(type);
|
|
73
|
-
if (upper === undefined) {
|
|
74
|
-
upper = type.toUpperCase();
|
|
75
|
-
typeUpperCache.set(type, upper);
|
|
76
|
-
}
|
|
77
|
-
return upper;
|
|
78
|
-
};
|
|
79
|
-
// Non-product helper entities that on-demand extraction / StepExporter
|
|
80
|
-
// need addressable in `byId`. These are not IfcProduct subtypes so the
|
|
81
|
-
// schema-driven IFCPRODUCT subtype check below cannot capture them.
|
|
82
|
-
// Without them, findPreferredGeometricRepresentationContextId() and
|
|
83
|
-
// findLengthUnitReference() fail because the entities are missing from
|
|
84
|
-
// the compact entity index.
|
|
85
|
-
const RELEVANT_NON_PRODUCT_HELPERS = new Set([
|
|
86
|
-
'IFCGEOMETRICREPRESENTATIONCONTEXT', 'IFCGEOMETRICREPRESENTATIONSUBCONTEXT',
|
|
87
|
-
'IFCUNITASSIGNMENT', 'IFCSIUNIT', 'IFCCONVERSIONBASEDUNIT',
|
|
88
|
-
'IFCDERIVEDUNIT', 'IFCDERIVEDUNITELEMENT', 'IFCMEASUREWITHUNIT',
|
|
89
|
-
'IFCDIMENSIONALEXPONENTS',
|
|
90
|
-
'IFCMAPCONVERSION', 'IFCPROJECTEDCRS',
|
|
91
|
-
'IFCMATERIALLAYER', 'IFCMATERIALLAYERSET', 'IFCMATERIALLAYERSETUSAGE',
|
|
92
|
-
'IFCMATERIALCONSTITUENTSET', 'IFCMATERIALCONSTITUENT',
|
|
93
|
-
'IFCMATERIALPROFILESET', 'IFCMATERIALPROFILE', 'IFCMATERIAL',
|
|
94
|
-
'IFCCLASSIFICATION', 'IFCCLASSIFICATIONREFERENCE',
|
|
95
|
-
'IFCDOCUMENTINFORMATION', 'IFCDOCUMENTREFERENCE',
|
|
96
|
-
]);
|
|
97
|
-
// Schema-driven inclusion: every IfcProduct subtype belongs in the
|
|
98
|
-
// EntityTable. The previous hardcoded enumeration of IFC4 building-
|
|
99
|
-
// element leaves (IFCWALL, IFCSLAB, …) and IFC4x3 infrastructure
|
|
100
|
-
// leaves (IFCREFERENT, IFCSIGNAL, IFCALIGNMENT, IFCPAVEMENT, …) drifted
|
|
101
|
-
// with every schema bump — new entities silently became CAT_SKIP and
|
|
102
|
-
// disappeared from the hierarchy panel. The generated schema registry
|
|
103
|
-
// already knows the full inheritance chain, so use it.
|
|
104
|
-
const RELEVANT_PRODUCT_ROOTS = new Set(['IFCPRODUCT']);
|
|
105
|
-
// IfcGroup family (IfcZone, IfcSystem, IfcDistributionSystem,
|
|
106
|
-
// IfcBuildingSystem, IfcDistributionCircuit, …). These are NOT
|
|
107
|
-
// IfcProduct subtypes, so without an explicit branch they fall through
|
|
108
|
-
// to CAT_SKIP and never enter the EntityTable — leaving their Name
|
|
109
|
-
// unresolvable (`getName` → '') and making them invisible to
|
|
110
|
-
// `getByType`. The Relationships card then shows "Group #<id>" and the
|
|
111
|
-
// lens/lists can't surface them. Route them into their own bucket so we
|
|
112
|
-
// can extract Name/LongName/ObjectType for the group label (#1075).
|
|
113
|
-
const GROUP_ROOTS = new Set(['IFCGROUP']);
|
|
114
|
-
// Category constants for the lookup cache
|
|
115
|
-
const CAT_SKIP = 0, CAT_SPATIAL = 1, CAT_GEOMETRY = 2, CAT_HIERARCHY_REL = 3, CAT_PROPERTY_REL = 4, CAT_PROPERTY_ENTITY = 5, CAT_ASSOCIATION_REL = 6, CAT_TYPE_OBJECT = 7, CAT_RELEVANT = 8, CAT_GROUP = 9;
|
|
116
|
-
/** Returns true if `upper` (already uppercased) is a subtype of any type in `set`. */
|
|
117
|
-
function isSubtypeOfAny(upper, set) {
|
|
118
|
-
const chain = getInheritanceChain(upper);
|
|
119
|
-
return chain.some(ancestor => set.has(ancestor.toUpperCase()));
|
|
120
|
-
}
|
|
121
|
-
// Cache: type name → category (avoids 4.4M .toUpperCase() calls)
|
|
122
|
-
const typeCategoryCache = new Map();
|
|
123
|
-
function getCategory(type) {
|
|
124
|
-
let cat = typeCategoryCache.get(type);
|
|
125
|
-
if (cat !== undefined)
|
|
126
|
-
return cat;
|
|
127
|
-
const upper = getTypeUpper(type);
|
|
128
|
-
if (SPATIAL_TYPES.has(upper) || isSubtypeOfAny(upper, SPATIAL_TYPES))
|
|
129
|
-
cat = CAT_SPATIAL;
|
|
130
|
-
else if (GEOMETRY_TYPES.has(upper) || isSubtypeOfAny(upper, GEOMETRY_TYPES))
|
|
131
|
-
cat = CAT_GEOMETRY;
|
|
132
|
-
else if (HIERARCHY_REL_TYPES.has(upper))
|
|
133
|
-
cat = CAT_HIERARCHY_REL;
|
|
134
|
-
else if (PROPERTY_REL_TYPES.has(upper))
|
|
135
|
-
cat = CAT_PROPERTY_REL;
|
|
136
|
-
else if (PROPERTY_ENTITY_TYPES.has(upper))
|
|
137
|
-
cat = CAT_PROPERTY_ENTITY;
|
|
138
|
-
else if (ASSOCIATION_REL_TYPES.has(upper))
|
|
139
|
-
cat = CAT_ASSOCIATION_REL;
|
|
140
|
-
else if (isIfcTypeLikeEntity(upper))
|
|
141
|
-
cat = CAT_TYPE_OBJECT;
|
|
142
|
-
else if (isSubtypeOfAny(upper, GROUP_ROOTS))
|
|
143
|
-
cat = CAT_GROUP;
|
|
144
|
-
else if (RELEVANT_NON_PRODUCT_HELPERS.has(upper)
|
|
145
|
-
|| isSubtypeOfAny(upper, RELEVANT_PRODUCT_ROOTS)
|
|
146
|
-
|| upper.startsWith('IFCREL'))
|
|
147
|
-
cat = CAT_RELEVANT;
|
|
148
|
-
else
|
|
149
|
-
cat = CAT_SKIP;
|
|
150
|
-
typeCategoryCache.set(type, cat);
|
|
151
|
-
return cat;
|
|
152
|
-
}
|
|
153
|
-
// Time-based yielding: yield to the main thread every ~80ms so geometry
|
|
154
|
-
// streaming callbacks can fire. This limits main-thread blocking to short
|
|
155
|
-
// bursts that don't starve geometry, while adding minimal overhead (~15 yields
|
|
156
|
-
// × ~1ms each ≈ 15ms total over the full parse).
|
|
157
|
-
const YIELD_INTERVAL_MS = Math.max(16, options.yieldIntervalMs ?? 80);
|
|
158
|
-
let lastYieldTime = performance.now();
|
|
159
|
-
const yieldIfNeeded = async () => {
|
|
160
|
-
const now = performance.now();
|
|
161
|
-
if (now - lastYieldTime >= YIELD_INTERVAL_MS) {
|
|
162
|
-
await yieldToEventLoop();
|
|
163
|
-
lastYieldTime = performance.now();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
emitDiagnostic(`parseLite start: totalEntities=${totalEntities} yieldInterval=${YIELD_INTERVAL_MS}ms`);
|
|
167
|
-
const spatialRefs = [];
|
|
168
|
-
const geometryRefs = [];
|
|
169
|
-
const relationshipRefs = [];
|
|
170
|
-
const propertyRelRefs = [];
|
|
171
|
-
const propertyContainerRefs = [];
|
|
172
|
-
const propertyAtomRefs = [];
|
|
173
|
-
const associationRelRefs = [];
|
|
174
|
-
const typeObjectRefs = [];
|
|
175
|
-
const otherRelevantRefs = [];
|
|
176
|
-
const groupRefs = [];
|
|
177
|
-
for (let i = 0; i < entityRefs.length; i++) {
|
|
178
|
-
if ((i & 0x3FF) === 0)
|
|
179
|
-
await yieldIfNeeded();
|
|
180
|
-
const ref = entityRefs[i];
|
|
181
|
-
// Categorize (cached — .toUpperCase() called once per unique type)
|
|
182
|
-
const cat = getCategory(ref.type);
|
|
183
|
-
const typeUpper = cat === CAT_PROPERTY_ENTITY ? getTypeUpper(ref.type) : '';
|
|
184
|
-
// ALL entities must be indexed in byType for on-demand extraction
|
|
185
|
-
// (e.g. IfcGeometricRepresentationContext, IfcSiUnit, IfcMaterialLayer).
|
|
186
|
-
// Only property atoms are optionally deferred for huge-file lazy loading.
|
|
187
|
-
const includeInPrimaryIndex = !deferPropertyAtomIndex || cat !== CAT_PROPERTY_ENTITY || PROPERTY_CONTAINER_TYPES.has(typeUpper);
|
|
188
|
-
if (includeInPrimaryIndex) {
|
|
189
|
-
// STEP convention is uppercase entity type names and every
|
|
190
|
-
// downstream consumer (schedule-extractor, property readers,
|
|
191
|
-
// test helpers) keys on uppercase. The tokenizer preserves
|
|
192
|
-
// original case though, so if a STEP writer ever emits
|
|
193
|
-
// mixed-case or lowercase types the index would miss on
|
|
194
|
-
// canonical lookups. Normalise once here — `getTypeUpper`
|
|
195
|
-
// is already cached by type name so the cost is ~0.
|
|
196
|
-
const typeKey = getTypeUpper(ref.type);
|
|
197
|
-
let typeList = byType.get(typeKey);
|
|
198
|
-
if (!typeList) {
|
|
199
|
-
typeList = [];
|
|
200
|
-
byType.set(typeKey, typeList);
|
|
201
|
-
}
|
|
202
|
-
typeList.push(ref.expressId);
|
|
203
|
-
}
|
|
204
|
-
if (cat === CAT_SPATIAL)
|
|
205
|
-
spatialRefs.push(ref);
|
|
206
|
-
else if (cat === CAT_GEOMETRY)
|
|
207
|
-
geometryRefs.push(ref);
|
|
208
|
-
else if (cat === CAT_HIERARCHY_REL)
|
|
209
|
-
relationshipRefs.push(ref);
|
|
210
|
-
else if (cat === CAT_PROPERTY_REL)
|
|
211
|
-
propertyRelRefs.push(ref);
|
|
212
|
-
else if (cat === CAT_PROPERTY_ENTITY) {
|
|
213
|
-
if (PROPERTY_CONTAINER_TYPES.has(typeUpper))
|
|
214
|
-
propertyContainerRefs.push(ref);
|
|
215
|
-
else
|
|
216
|
-
propertyAtomRefs.push(ref);
|
|
217
|
-
}
|
|
218
|
-
else if (cat === CAT_ASSOCIATION_REL)
|
|
219
|
-
associationRelRefs.push(ref);
|
|
220
|
-
else if (cat === CAT_TYPE_OBJECT)
|
|
221
|
-
typeObjectRefs.push(ref);
|
|
222
|
-
else if (cat === CAT_GROUP)
|
|
223
|
-
groupRefs.push(ref);
|
|
224
|
-
else if (cat === CAT_RELEVANT)
|
|
225
|
-
otherRelevantRefs.push(ref);
|
|
226
|
-
}
|
|
227
|
-
logPhase(`categorize ${totalEntities} → spatial:${spatialRefs.length} geom:${geometryRefs.length} rel:${relationshipRefs.length} propRel:${propertyRelRefs.length} propContainers:${propertyContainerRefs.length} propAtoms:${propertyAtomRefs.length} assocRel:${associationRelRefs.length} type:${typeObjectRefs.length} group:${groupRefs.length} other:${otherRelevantRefs.length}`);
|
|
228
|
-
// ALL entity refs must be indexed in byId so that on-demand extraction
|
|
229
|
-
// can look up any entity by expressId (e.g. IfcUnitAssignment,
|
|
230
|
-
// IfcGeometricRepresentationContext, IfcSiUnit, IfcLocalPlacement, etc.).
|
|
231
|
-
// Only property atoms are optionally deferred for huge-file lazy loading.
|
|
232
|
-
const indexedRefs = deferPropertyAtomIndex
|
|
233
|
-
? entityRefs.filter(ref => {
|
|
234
|
-
const cat = getCategory(ref.type);
|
|
235
|
-
return cat !== CAT_PROPERTY_ENTITY || PROPERTY_CONTAINER_TYPES.has(getTypeUpper(ref.type));
|
|
236
|
-
})
|
|
237
|
-
: entityRefs;
|
|
238
|
-
emitDiagnostic(`index input: indexedRefs=${indexedRefs.length} deferredPropertyAtoms=${deferPropertyAtomIndex ? propertyAtomRefs.length : 0}`);
|
|
239
|
-
// Keep every indexed entity available for on-demand reference resolution.
|
|
240
|
-
const compactByIdIndex = await buildCompactEntityIndexAsync(indexedRefs);
|
|
241
|
-
logPhase('compact entity index');
|
|
242
|
-
// Create entity table builder with EXACT capacity (not totalEntities which
|
|
243
|
-
// includes millions of geometry-representation entities we don't store).
|
|
244
|
-
// For a 14M entity file, this reduces allocation from ~546MB to ~20MB.
|
|
245
|
-
const relevantCount = spatialRefs.length + geometryRefs.length + typeObjectRefs.length
|
|
246
|
-
+ relationshipRefs.length + groupRefs.length + otherRelevantRefs.length;
|
|
247
|
-
const entityTableBuilder = new EntityTableBuilder(relevantCount, strings);
|
|
248
|
-
const entityIndex = {
|
|
249
|
-
byId: compactByIdIndex,
|
|
250
|
-
byType,
|
|
251
|
-
};
|
|
252
|
-
// === TARGETED PARSING using batch byte-level extraction ===
|
|
253
|
-
// Uses 2 TextDecoder.decode() calls total for ALL entity GlobalIds/Names
|
|
254
|
-
// (instead of per-entity calls), and pure byte scanning for relationships.
|
|
255
|
-
options.onProgress?.({ phase: 'parsing entities', percent: 10 });
|
|
256
|
-
const extractor = new EntityExtractor(uint8Buffer);
|
|
257
|
-
// Spatial entities: small count, use extractEntity for full accuracy
|
|
258
|
-
const parsedEntityData = new Map();
|
|
259
|
-
for (const ref of spatialRefs) {
|
|
260
|
-
const entity = extractor.extractEntity(ref);
|
|
261
|
-
if (entity) {
|
|
262
|
-
const attrs = entity.attributes || [];
|
|
263
|
-
parsedEntityData.set(ref.expressId, {
|
|
264
|
-
globalId: typeof attrs[0] === 'string' ? attrs[0] : '',
|
|
265
|
-
name: typeof attrs[2] === 'string' ? attrs[2] : '',
|
|
266
|
-
});
|
|
267
|
-
}
|
|
28
|
+
/** Internal implementation shared by public refs and parser-worker columns. */
|
|
29
|
+
export async function parseColumnarInput(buffer, input, options = {}) {
|
|
30
|
+
const startTime = performance.now();
|
|
31
|
+
const uint8Buffer = new Uint8Array(buffer);
|
|
32
|
+
const totalEntities = Array.isArray(input) ? input.length : input.expressIds.length;
|
|
33
|
+
// Phase timing for performance telemetry
|
|
34
|
+
let phaseStart = startTime;
|
|
35
|
+
const emitDiagnostic = (message) => {
|
|
36
|
+
options.onDiagnostic?.(message);
|
|
37
|
+
};
|
|
38
|
+
const logPhase = (name) => {
|
|
39
|
+
const now = performance.now();
|
|
40
|
+
const elapsed = Math.round(now - phaseStart);
|
|
41
|
+
console.log(`[parseLite] ${name}: ${elapsed}ms`);
|
|
42
|
+
emitDiagnostic(`${name}: ${elapsed}ms`);
|
|
43
|
+
phaseStart = now;
|
|
44
|
+
};
|
|
45
|
+
options.onProgress?.({ phase: 'building', percent: 0 });
|
|
46
|
+
// Capture verbatim HEADER fields so a round-trip export can reproduce
|
|
47
|
+
// the source FILE_DESCRIPTION items + exact FILE_SCHEMA token instead
|
|
48
|
+
// of regenerating a fresh ifc-lite header. Cheap: the scan stops at the
|
|
49
|
+
// header's ENDSEC, so the DATA section is never touched.
|
|
50
|
+
const sourceHeader = parseSourceHeader(uint8Buffer);
|
|
51
|
+
// Schema version comes from that header's FILE_SCHEMA declaration, not
|
|
52
|
+
// from a substring scan of the raw bytes — exporter product names in
|
|
53
|
+
// FILE_NAME free text carry schema tokens too (issue #3278).
|
|
54
|
+
const schemaVersion = detectSchemaVersion(uint8Buffer, sourceHeader);
|
|
55
|
+
// Initialize builders (entity table capacity set after categorization below)
|
|
56
|
+
const strings = new StringTable();
|
|
57
|
+
const propertyTableBuilder = new PropertyTableBuilder(strings);
|
|
58
|
+
const quantityTableBuilder = new QuantityTableBuilder(strings);
|
|
59
|
+
const relationshipGraphBuilder = new RelationshipGraphBuilder();
|
|
60
|
+
logPhase('init builders');
|
|
61
|
+
// Time-based yielding: yield to the main thread every ~80ms so geometry
|
|
62
|
+
// streaming callbacks can fire. This limits main-thread blocking to short
|
|
63
|
+
// bursts that don't starve geometry, while adding minimal overhead (~15 yields
|
|
64
|
+
// × ~1ms each ≈ 15ms total over the full parse).
|
|
65
|
+
const YIELD_INTERVAL_MS = Math.max(16, options.yieldIntervalMs ?? 80);
|
|
66
|
+
let lastYieldTime = performance.now();
|
|
67
|
+
const yieldIfNeeded = async () => {
|
|
68
|
+
const now = performance.now();
|
|
69
|
+
if (now - lastYieldTime >= YIELD_INTERVAL_MS) {
|
|
70
|
+
await yieldToEventLoop();
|
|
71
|
+
lastYieldTime = performance.now();
|
|
268
72
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
73
|
+
};
|
|
74
|
+
emitDiagnostic(`parseLite start: totalEntities=${totalEntities} yieldInterval=${YIELD_INTERVAL_MS}ms`);
|
|
75
|
+
const prepared = await prepareColumnarEntities(input, options.deferPropertyAtomIndex === true, yieldIfNeeded);
|
|
76
|
+
const { byType, getTypeUpper, spatialRefs, geometryRefs, relationshipRefs, propertyRelRefs, propertyContainerRefs, associationRelRefs, typeObjectRefs, otherRelevantRefs, groupRefs } = prepared;
|
|
77
|
+
logPhase(`categorize ${totalEntities} → spatial:${spatialRefs.length} geom:${geometryRefs.length} rel:${relationshipRefs.length} propRel:${propertyRelRefs.length} propContainers:${propertyContainerRefs.length} propAtoms:${prepared.propertyAtomCount} assocRel:${associationRelRefs.length} type:${typeObjectRefs.length} group:${groupRefs.length} other:${otherRelevantRefs.length}`);
|
|
78
|
+
emitDiagnostic(`index input: indexedRefs=${prepared.indexedCount} deferredPropertyAtoms=${options.deferPropertyAtomIndex ? prepared.propertyAtomCount : 0}`);
|
|
79
|
+
const compactByIdIndex = await prepared.buildPrimaryIndex();
|
|
80
|
+
logPhase('compact entity index');
|
|
81
|
+
// Create entity table builder with EXACT capacity (not totalEntities which
|
|
82
|
+
// includes millions of geometry-representation entities we don't store).
|
|
83
|
+
// For a 14M entity file, this reduces allocation from ~546MB to ~20MB.
|
|
84
|
+
const relevantCount = spatialRefs.length + geometryRefs.length + typeObjectRefs.length
|
|
85
|
+
+ relationshipRefs.length + groupRefs.length + otherRelevantRefs.length;
|
|
86
|
+
const entityTableBuilder = new EntityTableBuilder(relevantCount, strings);
|
|
87
|
+
const entityIndex = {
|
|
88
|
+
byId: compactByIdIndex,
|
|
89
|
+
byType,
|
|
90
|
+
};
|
|
91
|
+
// === TARGETED PARSING using batch byte-level extraction ===
|
|
92
|
+
// Uses 2 TextDecoder.decode() calls total for ALL entity GlobalIds/Names
|
|
93
|
+
// (instead of per-entity calls), and pure byte scanning for relationships.
|
|
94
|
+
options.onProgress?.({ phase: 'parsing entities', percent: 10 });
|
|
95
|
+
const extractor = new EntityExtractor(uint8Buffer);
|
|
96
|
+
// Spatial entities: small count, use extractEntity for full accuracy
|
|
97
|
+
const parsedEntityData = new Map();
|
|
98
|
+
for (const ref of spatialRefs) {
|
|
99
|
+
const entity = extractor.extractEntity(ref);
|
|
100
|
+
if (entity) {
|
|
101
|
+
const attrs = entity.attributes || [];
|
|
283
102
|
parsedEntityData.set(ref.expressId, {
|
|
284
|
-
globalId:
|
|
285
|
-
name:
|
|
103
|
+
globalId: typeof attrs[0] === 'string' ? attrs[0] : '',
|
|
104
|
+
name: typeof attrs[2] === 'string' ? attrs[2] : '',
|
|
286
105
|
});
|
|
287
|
-
groupExtra.set(ref.expressId, { description: root.description, objectType: root.objectType });
|
|
288
106
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
107
|
+
}
|
|
108
|
+
logPhase('spatial entities');
|
|
109
|
+
await yieldIfNeeded();
|
|
110
|
+
// Group family (IfcZone / IfcSystem / IfcDistributionSystem / …): small
|
|
111
|
+
// count, full extract so we can resolve Name + LongName + ObjectType for
|
|
112
|
+
// the group label. Name is often empty on these — the human label lives
|
|
113
|
+
// in LongName (IfcZone/IfcDistributionSystem) — so fall back to it, and
|
|
114
|
+
// keep ObjectType (e.g. a system designation) for richer display. (#1075)
|
|
115
|
+
const groupExtra = new Map();
|
|
116
|
+
for (const ref of groupRefs) {
|
|
117
|
+
const entity = extractor.extractEntity(ref);
|
|
118
|
+
if (!entity)
|
|
119
|
+
continue;
|
|
120
|
+
const root = extractRootAttributesFromEntity(entity);
|
|
121
|
+
const longName = pickLongName(entity);
|
|
122
|
+
parsedEntityData.set(ref.expressId, {
|
|
123
|
+
globalId: root.globalId,
|
|
124
|
+
name: root.name || longName,
|
|
125
|
+
});
|
|
126
|
+
groupExtra.set(ref.expressId, { description: root.description, objectType: root.objectType });
|
|
127
|
+
}
|
|
128
|
+
logPhase('group entities');
|
|
129
|
+
await yieldIfNeeded();
|
|
130
|
+
// Geometry + type object entities: batch extract GlobalId+Name with 2 TextDecoder calls
|
|
131
|
+
options.onProgress?.({ phase: 'parsing geometry names', percent: 12 });
|
|
132
|
+
const geomData = await batchExtractGlobalIdAndName(uint8Buffer, geometryRefs, yieldIfNeeded);
|
|
133
|
+
for (const [id, data] of geomData)
|
|
134
|
+
parsedEntityData.set(id, data);
|
|
135
|
+
await yieldIfNeeded();
|
|
136
|
+
const typeData = await batchExtractGlobalIdAndName(uint8Buffer, typeObjectRefs, yieldIfNeeded);
|
|
137
|
+
for (const [id, data] of typeData)
|
|
138
|
+
parsedEntityData.set(id, data);
|
|
139
|
+
logPhase('batch geom GlobalId+Name');
|
|
140
|
+
await yieldIfNeeded();
|
|
141
|
+
// Other relevant products (IfcSpatialZone, IfcVirtualElement, IfcGrid,
|
|
142
|
+
// IfcAnnotation, …): batch GlobalId+Name so they show their Name in the
|
|
143
|
+
// hierarchy / lists instead of nothing. Previously these were added to
|
|
144
|
+
// the EntityTable with an empty name (parsedEntityData never covered
|
|
145
|
+
// this bucket), so e.g. IfcSpatialZone listed without a label. (#1075)
|
|
146
|
+
const otherData = await batchExtractGlobalIdAndName(uint8Buffer, otherRelevantRefs, yieldIfNeeded);
|
|
147
|
+
for (const [id, data] of otherData)
|
|
148
|
+
parsedEntityData.set(id, data);
|
|
149
|
+
logPhase('batch other-relevant GlobalId+Name');
|
|
150
|
+
await yieldIfNeeded();
|
|
151
|
+
// Relationships: byte-level scanning (numbers only, no TextDecoder)
|
|
152
|
+
options.onProgress?.({ phase: 'parsing relationships', percent: 20 });
|
|
153
|
+
for (let i = 0; i < relationshipRefs.length; i++) {
|
|
154
|
+
if ((i & 0x3FF) === 0)
|
|
155
|
+
await yieldIfNeeded();
|
|
156
|
+
const ref = relationshipRefs[i];
|
|
157
|
+
const typeUpper = getTypeUpper(ref.type);
|
|
158
|
+
const rel = extractRelFast(uint8Buffer, ref.byteOffset, ref.byteLength, typeUpper);
|
|
159
|
+
if (rel) {
|
|
160
|
+
const relType = REL_TYPE_MAP[typeUpper];
|
|
161
|
+
if (relType) {
|
|
162
|
+
for (const targetId of rel.relatedObjects) {
|
|
163
|
+
relationshipGraphBuilder.addEdge(rel.relatingObject, targetId, relType, ref.expressId);
|
|
326
164
|
}
|
|
327
165
|
}
|
|
328
166
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
entityTableBuilder.add(ref.expressId, ref.type, entityData?.globalId || '', entityData?.name || '', '', // description
|
|
339
|
-
'', // objectType
|
|
340
|
-
hasGeometry, isType);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
addEntityBatch(spatialRefs, false, false);
|
|
344
|
-
addEntityBatch(geometryRefs, true, false);
|
|
345
|
-
addEntityBatch(typeObjectRefs, false, true);
|
|
346
|
-
addEntityBatch(relationshipRefs, false, false);
|
|
347
|
-
addEntityBatch(otherRelevantRefs, false, false);
|
|
348
|
-
// Groups carry Description + ObjectType (the system designation), which
|
|
349
|
-
// the shared addEntityBatch drops as ''. Add them with the extra fields
|
|
350
|
-
// so the Properties title / lists / lens can surface them. (#1075)
|
|
351
|
-
for (const ref of groupRefs) {
|
|
167
|
+
}
|
|
168
|
+
logPhase('byte-level relationships');
|
|
169
|
+
// === BUILD ENTITY TABLE from categorized arrays ===
|
|
170
|
+
// Instead of iterating ALL 4.4M entityRefs, iterate only categorized arrays
|
|
171
|
+
// (~100K-200K total). This eliminates a 200-300ms loop over 4.4M items.
|
|
172
|
+
options.onProgress?.({ phase: 'building entities', percent: 30 });
|
|
173
|
+
// Helper to add entities with pre-parsed data
|
|
174
|
+
const addEntityBatch = (refs, hasGeometry, isType) => {
|
|
175
|
+
for (const ref of refs) {
|
|
352
176
|
const entityData = parsedEntityData.get(ref.expressId);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
logPhase('add entity batches');
|
|
357
|
-
const entityTable = entityTableBuilder.build();
|
|
358
|
-
logPhase('entity table build()');
|
|
359
|
-
// Empty property/quantity tables - use on-demand extraction instead
|
|
360
|
-
const propertyTable = propertyTableBuilder.build();
|
|
361
|
-
const quantityTable = quantityTableBuilder.build();
|
|
362
|
-
// Build intermediate relationship graph (spatial/hierarchy edges only).
|
|
363
|
-
// Property/association edges are added later; final graph is rebuilt at the end.
|
|
364
|
-
const hierarchyRelGraph = relationshipGraphBuilder.build();
|
|
365
|
-
logPhase('hierarchy rel graph build()');
|
|
366
|
-
await yieldIfNeeded();
|
|
367
|
-
// === EXTRACT LENGTH UNIT SCALE ===
|
|
368
|
-
options.onProgress?.({ phase: 'extracting units', percent: 85 });
|
|
369
|
-
const lengthUnitScale = extractLengthUnitScale(uint8Buffer, entityIndex);
|
|
370
|
-
// === BUILD SPATIAL HIERARCHY ===
|
|
371
|
-
options.onProgress?.({ phase: 'building hierarchy', percent: 90 });
|
|
372
|
-
let spatialHierarchy;
|
|
373
|
-
try {
|
|
374
|
-
const hierarchyBuilder = new SpatialHierarchyBuilder();
|
|
375
|
-
spatialHierarchy = hierarchyBuilder.build(entityTable, hierarchyRelGraph, strings, uint8Buffer, entityIndex, lengthUnitScale);
|
|
376
|
-
logPhase('spatial hierarchy');
|
|
177
|
+
entityTableBuilder.add(ref.expressId, ref.type, entityData?.globalId || '', entityData?.name || '', '', // description
|
|
178
|
+
'', // objectType
|
|
179
|
+
hasGeometry, isType);
|
|
377
180
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
181
|
+
};
|
|
182
|
+
addEntityBatch(spatialRefs, false, false);
|
|
183
|
+
addEntityBatch(geometryRefs, true, false);
|
|
184
|
+
addEntityBatch(typeObjectRefs, false, true);
|
|
185
|
+
addEntityBatch(relationshipRefs, false, false);
|
|
186
|
+
addEntityBatch(otherRelevantRefs, false, false);
|
|
187
|
+
// Groups carry Description + ObjectType (the system designation), which
|
|
188
|
+
// the shared addEntityBatch drops as ''. Add them with the extra fields
|
|
189
|
+
// so the Properties title / lists / lens can surface them. (#1075)
|
|
190
|
+
for (const ref of groupRefs) {
|
|
191
|
+
const entityData = parsedEntityData.get(ref.expressId);
|
|
192
|
+
const extra = groupExtra.get(ref.expressId);
|
|
193
|
+
entityTableBuilder.add(ref.expressId, ref.type, entityData?.globalId || '', entityData?.name || '', extra?.description || '', extra?.objectType || '', false, false);
|
|
194
|
+
}
|
|
195
|
+
logPhase('add entity batches');
|
|
196
|
+
const entityTable = entityTableBuilder.build();
|
|
197
|
+
logPhase('entity table build()');
|
|
198
|
+
// Empty property/quantity tables - use on-demand extraction instead
|
|
199
|
+
const propertyTable = propertyTableBuilder.build();
|
|
200
|
+
const quantityTable = quantityTableBuilder.build();
|
|
201
|
+
// Build intermediate relationship graph (spatial/hierarchy edges only).
|
|
202
|
+
// Property/association edges are added later; final graph is rebuilt at the end.
|
|
203
|
+
const hierarchyRelGraph = relationshipGraphBuilder.build();
|
|
204
|
+
logPhase('hierarchy rel graph build()');
|
|
205
|
+
await yieldIfNeeded();
|
|
206
|
+
// === EXTRACT LENGTH UNIT SCALE ===
|
|
207
|
+
options.onProgress?.({ phase: 'extracting units', percent: 85 });
|
|
208
|
+
const lengthUnitScale = extractLengthUnitScale(uint8Buffer, entityIndex);
|
|
209
|
+
// === BUILD SPATIAL HIERARCHY ===
|
|
210
|
+
options.onProgress?.({ phase: 'building hierarchy', percent: 90 });
|
|
211
|
+
let spatialHierarchy;
|
|
212
|
+
try {
|
|
213
|
+
const hierarchyBuilder = new SpatialHierarchyBuilder();
|
|
214
|
+
spatialHierarchy = hierarchyBuilder.build(entityTable, hierarchyRelGraph, strings, uint8Buffer, entityIndex, lengthUnitScale);
|
|
215
|
+
logPhase('spatial hierarchy');
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.warn('[ColumnarParser] Failed to build spatial hierarchy:', error);
|
|
219
|
+
}
|
|
220
|
+
// === EMIT SPATIAL HIERARCHY EARLY ===
|
|
221
|
+
// The hierarchy panel can render immediately while property/association
|
|
222
|
+
// parsing continues. This lets the panel appear at the same time as
|
|
223
|
+
// geometry streaming completes.
|
|
224
|
+
// ONE accessor, shared by the store field and the entity source.
|
|
225
|
+
//
|
|
226
|
+
// These used to be built separately -- `BufferEntitySource` got the raw
|
|
227
|
+
// `uint8Buffer` and wrapped it in its own accessor, while `source` got
|
|
228
|
+
// another. That is invisible while both are resident views over the
|
|
229
|
+
// same bytes, and fatal once the source can switch to compressed
|
|
230
|
+
// storage in place (#2183): `getEntity` would keep reading its private
|
|
231
|
+
// resident accessor, so the original buffer would never be released and
|
|
232
|
+
// the store would serve entities from one representation and properties
|
|
233
|
+
// from the other. Measured: with two accessors the buffer survives GC
|
|
234
|
+
// after the swap; with one it is collected.
|
|
235
|
+
const source = contiguousSourceBytes(uint8Buffer);
|
|
236
|
+
const entitySource = new BufferEntitySource(source, entityIndex);
|
|
237
|
+
const earlyStore = {
|
|
238
|
+
fileSize: buffer.byteLength,
|
|
239
|
+
schemaVersion,
|
|
240
|
+
sourceHeader,
|
|
241
|
+
entityCount: totalEntities,
|
|
242
|
+
parseTime: performance.now() - startTime,
|
|
243
|
+
source,
|
|
244
|
+
entityIndex,
|
|
245
|
+
strings,
|
|
246
|
+
entities: entityTable,
|
|
247
|
+
properties: propertyTable,
|
|
248
|
+
quantities: quantityTable,
|
|
249
|
+
relationships: hierarchyRelGraph,
|
|
250
|
+
spatialHierarchy,
|
|
251
|
+
lengthUnitScale,
|
|
252
|
+
getEntity(expressId) { return entitySource.getEntity(expressId); },
|
|
253
|
+
getEntitiesByType(typeName) { return entitySource.getEntitiesByType(typeName); },
|
|
254
|
+
getProperties(expressId) { return this.properties.getForEntity(expressId); },
|
|
255
|
+
getQuantities(expressId) { return this.quantities.getForEntity(expressId); },
|
|
256
|
+
};
|
|
257
|
+
options.onSpatialReady?.(earlyStore);
|
|
258
|
+
await yieldIfNeeded(); // Let geometry process after hierarchy emission
|
|
259
|
+
// === DEFERRED: Parse property and association relationships ===
|
|
260
|
+
// These are NOT needed for the spatial hierarchy panel.
|
|
261
|
+
options.onProgress?.({ phase: 'parsing property refs', percent: 92 });
|
|
262
|
+
const onDemandPropertyMap = new Map();
|
|
263
|
+
const onDemandQuantityMap = new Map();
|
|
264
|
+
// Pre-build Sets of property set / quantity set IDs from already-categorized refs.
|
|
265
|
+
// This replaces 252K binary searches on the 14M compact entity index with O(1) Set lookups.
|
|
266
|
+
const propertySetIds = new Set();
|
|
267
|
+
const quantitySetIds = new Set();
|
|
268
|
+
for (const ref of propertyContainerRefs) {
|
|
269
|
+
const tu = getTypeUpper(ref.type);
|
|
270
|
+
if (tu === 'IFCPROPERTYSET')
|
|
271
|
+
propertySetIds.add(ref.expressId);
|
|
272
|
+
else if (tu === 'IFCELEMENTQUANTITY')
|
|
273
|
+
quantitySetIds.add(ref.expressId);
|
|
274
|
+
}
|
|
275
|
+
// Property rels: byte-level scanning + addEdge (now fast with SoA builder).
|
|
276
|
+
let totalPropRelObjects = 0;
|
|
277
|
+
for (let pi = 0; pi < propertyRelRefs.length; pi++) {
|
|
278
|
+
if ((pi & 0x3FF) === 0)
|
|
279
|
+
await yieldIfNeeded();
|
|
280
|
+
const ref = propertyRelRefs[pi];
|
|
281
|
+
const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
|
|
282
|
+
if (result) {
|
|
283
|
+
const { relatedObjects, relatingDefs } = result;
|
|
284
|
+
totalPropRelObjects += relatedObjects.length;
|
|
285
|
+
// RelatingPropertyDefinition is IfcPropertySetDefinitionSelect, whose
|
|
286
|
+
// second alternative (IfcPropertySetDefinitionSet) is a SET of pset/
|
|
287
|
+
// qset definitions — `relatingDefs` has more than one entry for that
|
|
288
|
+
// case. Every entry in the group applies to every related object.
|
|
289
|
+
for (const relatingDef of relatingDefs) {
|
|
290
|
+
for (const objId of relatedObjects) {
|
|
291
|
+
relationshipGraphBuilder.addEdge(relatingDef, objId, RelationshipType.DefinesByProperties, ref.expressId);
|
|
292
|
+
}
|
|
293
|
+
// Build on-demand property/quantity maps using pre-built Sets (O(1) vs binary search)
|
|
294
|
+
const isPropSet = propertySetIds.has(relatingDef);
|
|
295
|
+
const isQtySet = !isPropSet && quantitySetIds.has(relatingDef);
|
|
296
|
+
if (isPropSet || isQtySet) {
|
|
297
|
+
const targetMap = isPropSet ? onDemandPropertyMap : onDemandQuantityMap;
|
|
451
298
|
for (const objId of relatedObjects) {
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
// Build on-demand property/quantity maps using pre-built Sets (O(1) vs binary search)
|
|
455
|
-
const isPropSet = propertySetIds.has(relatingDef);
|
|
456
|
-
const isQtySet = !isPropSet && quantitySetIds.has(relatingDef);
|
|
457
|
-
if (isPropSet || isQtySet) {
|
|
458
|
-
const targetMap = isPropSet ? onDemandPropertyMap : onDemandQuantityMap;
|
|
459
|
-
for (const objId of relatedObjects) {
|
|
460
|
-
addOnDemandRef(targetMap, objId, relatingDef);
|
|
461
|
-
}
|
|
299
|
+
addOnDemandRef(targetMap, objId, relatingDef);
|
|
462
300
|
}
|
|
463
301
|
}
|
|
464
302
|
}
|
|
465
303
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
304
|
+
}
|
|
305
|
+
await yieldIfNeeded();
|
|
306
|
+
// Association rels: byte-level scanning, no addEdge (same reasoning as property rels)
|
|
307
|
+
options.onProgress?.({ phase: 'parsing associations', percent: 95 });
|
|
308
|
+
const onDemandClassificationMap = new Map();
|
|
309
|
+
const onDemandMaterialMap = new Map();
|
|
310
|
+
const onDemandDocumentMap = new Map();
|
|
311
|
+
// Determinism rule for elements with MULTIPLE IfcRelAssociatesMaterial:
|
|
312
|
+
// the map keeps EVERY association (list-valued), ordered by rel express
|
|
313
|
+
// id, so list[0] — the "primary" — is the RelatingMaterial of the
|
|
314
|
+
// LOWEST rel express id regardless of file order. The cache rebuild
|
|
315
|
+
// (viewer spatialHierarchy rebuildOnDemandMaps) applies the same rule
|
|
316
|
+
// via edge relationshipIds, so fresh-parse and cache-load agree.
|
|
317
|
+
const materialRelIds = new Map();
|
|
318
|
+
for (let i = 0; i < associationRelRefs.length; i++) {
|
|
319
|
+
if ((i & 0x3FF) === 0)
|
|
320
|
+
await yieldIfNeeded();
|
|
321
|
+
const ref = associationRelRefs[i];
|
|
322
|
+
const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
|
|
323
|
+
if (result) {
|
|
324
|
+
// IfcRelAssociates{Material,Classification,Document}'s Relating*
|
|
325
|
+
// selects are always single refs (unlike RelatingPropertyDefinition
|
|
326
|
+
// above, none of IfcMaterialSelect/IfcClassificationSelect/
|
|
327
|
+
// IfcDocumentSelect admit a SET alternative) — take the one entry.
|
|
328
|
+
const { relatedObjects, relatingDefs } = result;
|
|
329
|
+
const relatingRef = relatingDefs[0];
|
|
330
|
+
const typeUpper = getTypeUpper(ref.type);
|
|
331
|
+
if (typeUpper === 'IFCRELASSOCIATESCLASSIFICATION') {
|
|
332
|
+
for (const objId of relatedObjects) {
|
|
333
|
+
addOnDemandRef(onDemandClassificationMap, objId, relatingRef);
|
|
334
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesClassification, ref.expressId);
|
|
497
335
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// Deliberately NOT deduped (#3782 review): buildMaterialUsageIndex
|
|
509
|
-
// already dedupes downstream (seenPerMaterial), per material-fraction-and-associations.test.ts.
|
|
510
|
-
let at = relIds.length;
|
|
511
|
-
while (at > 0 && relIds[at - 1] > ref.expressId)
|
|
512
|
-
at--;
|
|
513
|
-
relIds.splice(at, 0, ref.expressId);
|
|
514
|
-
list.splice(at, 0, relatingRef);
|
|
515
|
-
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesMaterial, ref.expressId);
|
|
336
|
+
}
|
|
337
|
+
else if (typeUpper === 'IFCRELASSOCIATESMATERIAL') {
|
|
338
|
+
for (const objId of relatedObjects) {
|
|
339
|
+
let list = onDemandMaterialMap.get(objId);
|
|
340
|
+
let relIds = materialRelIds.get(objId);
|
|
341
|
+
if (!list || !relIds) {
|
|
342
|
+
list = [];
|
|
343
|
+
onDemandMaterialMap.set(objId, list);
|
|
344
|
+
relIds = [];
|
|
345
|
+
materialRelIds.set(objId, relIds);
|
|
516
346
|
}
|
|
347
|
+
// Deliberately NOT deduped (#3782 review): buildMaterialUsageIndex
|
|
348
|
+
// already dedupes downstream (seenPerMaterial), per material-fraction-and-associations.test.ts.
|
|
349
|
+
let at = relIds.length;
|
|
350
|
+
while (at > 0 && relIds[at - 1] > ref.expressId)
|
|
351
|
+
at--;
|
|
352
|
+
relIds.splice(at, 0, ref.expressId);
|
|
353
|
+
list.splice(at, 0, relatingRef);
|
|
354
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesMaterial, ref.expressId);
|
|
517
355
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
356
|
+
}
|
|
357
|
+
else if (typeUpper === 'IFCRELASSOCIATESDOCUMENT') {
|
|
358
|
+
for (const objId of relatedObjects) {
|
|
359
|
+
addOnDemandRef(onDemandDocumentMap, objId, relatingRef);
|
|
360
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesDocument, ref.expressId);
|
|
523
361
|
}
|
|
524
362
|
}
|
|
525
363
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return this
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return this
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
364
|
+
}
|
|
365
|
+
logPhase('property+association rels');
|
|
366
|
+
// Rebuild relationship graph with ALL edges (hierarchy + property + association)
|
|
367
|
+
const fullRelationshipGraph = relationshipGraphBuilder.build();
|
|
368
|
+
logPhase('relationship graph build()');
|
|
369
|
+
let deferredEntityIndex;
|
|
370
|
+
if (options.deferPropertyAtomIndex && prepared.propertyAtomCount > 0) {
|
|
371
|
+
options.onProgress?.({ phase: 'indexing property atoms', percent: 98 });
|
|
372
|
+
deferredEntityIndex = await prepared.buildDeferredIndex();
|
|
373
|
+
logPhase('deferred property atom index');
|
|
374
|
+
}
|
|
375
|
+
const parseTime = performance.now() - startTime;
|
|
376
|
+
options.onProgress?.({ phase: 'complete', percent: 100 });
|
|
377
|
+
const finalStore = {
|
|
378
|
+
...earlyStore,
|
|
379
|
+
parseTime,
|
|
380
|
+
relationships: fullRelationshipGraph,
|
|
381
|
+
deferredEntityIndex,
|
|
382
|
+
onDemandPropertyMap,
|
|
383
|
+
onDemandQuantityMap,
|
|
384
|
+
onDemandClassificationMap,
|
|
385
|
+
onDemandMaterialMap,
|
|
386
|
+
onDemandDocumentMap,
|
|
387
|
+
lengthUnitScale,
|
|
388
|
+
getEntity(expressId) { return entitySource.getEntity(expressId); },
|
|
389
|
+
getEntitiesByType(typeName) { return entitySource.getEntitiesByType(typeName); },
|
|
390
|
+
getProperties(expressId) {
|
|
391
|
+
if (onDemandPropertyMap.size > 0)
|
|
392
|
+
return extractPropertiesOnDemand(this, expressId);
|
|
393
|
+
return this.properties.getForEntity(expressId);
|
|
394
|
+
},
|
|
395
|
+
getQuantities(expressId) {
|
|
396
|
+
if (onDemandQuantityMap.size > 0)
|
|
397
|
+
return extractQuantitiesOnDemand(this, expressId);
|
|
398
|
+
return this.quantities.getForEntity(expressId);
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
return finalStore;
|
|
402
|
+
}
|
|
403
|
+
export class ColumnarParser {
|
|
404
|
+
/**
|
|
405
|
+
* Parse IFC file into columnar data store
|
|
406
|
+
*
|
|
407
|
+
* Uses fast semicolon-based scanning with on-demand property extraction.
|
|
408
|
+
* Properties are parsed lazily when accessed, not upfront.
|
|
409
|
+
* This provides instant UI responsiveness even for very large files.
|
|
410
|
+
*/
|
|
411
|
+
async parseLite(buffer, entityRefs, options = {}) {
|
|
412
|
+
return parseColumnarInput(buffer, entityRefs, options);
|
|
563
413
|
}
|
|
564
414
|
/**
|
|
565
415
|
* Extract properties for a single entity ON-DEMAND
|
|
@@ -665,235 +515,6 @@ export function extractQuantitiesOnDemand(store, entityId) {
|
|
|
665
515
|
const parser = new ColumnarParser();
|
|
666
516
|
return parser.extractQuantitiesOnDemand(store, entityId);
|
|
667
517
|
}
|
|
668
|
-
function getEntityRefFromStore(store, expressId) {
|
|
669
|
-
return store.entityIndex.byId.get(expressId) ?? store.deferredEntityIndex?.get(expressId);
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Extract entity attributes on-demand from source buffer.
|
|
673
|
-
* Returns globalId, name, description, objectType, tag mapped by schema name
|
|
674
|
-
* (see {@link extractRootAttributesFromEntity}), so the result stays correct
|
|
675
|
-
* for entity types whose attribute order differs from the IfcElement layout.
|
|
676
|
-
* This is used for entities that weren't fully parsed during initial load.
|
|
677
|
-
*/
|
|
678
|
-
export function extractEntityAttributesOnDemand(store, entityId) {
|
|
679
|
-
const ref = store.entityIndex.byId.get(entityId);
|
|
680
|
-
if (!ref) {
|
|
681
|
-
return { globalId: '', name: '', description: '', objectType: '', tag: '' };
|
|
682
|
-
}
|
|
683
|
-
const extractor = new EntityExtractor(store.source);
|
|
684
|
-
const entity = extractor.extractEntity(ref);
|
|
685
|
-
if (!entity) {
|
|
686
|
-
return { globalId: '', name: '', description: '', objectType: '', tag: '' };
|
|
687
|
-
}
|
|
688
|
-
return extractRootAttributesFromEntity(entity);
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Extract ALL named entity attributes on-demand from source buffer.
|
|
692
|
-
* Uses the IFC schema to map attribute indices to names.
|
|
693
|
-
* Returns only string/enum attributes, skipping references and structural attributes.
|
|
694
|
-
*/
|
|
695
|
-
export function extractAllEntityAttributes(store, entityId) {
|
|
696
|
-
const ref = store.entityIndex.byId.get(entityId);
|
|
697
|
-
if (!ref)
|
|
698
|
-
return [];
|
|
699
|
-
const extractor = new EntityExtractor(store.source);
|
|
700
|
-
const entity = extractor.extractEntity(ref);
|
|
701
|
-
if (!entity)
|
|
702
|
-
return [];
|
|
703
|
-
const attrs = entity.attributes || [];
|
|
704
|
-
// Use properly-cased type name from entity table (IfcTypeEnumToString)
|
|
705
|
-
// instead of ref.type which is UPPERCASE from STEP (e.g., IFCWALLSTANDARDCASE)
|
|
706
|
-
// and breaks multi-word type normalization in getAttributeNames.
|
|
707
|
-
// For resource-level entities (IfcTask, IfcTaskTime, IfcMaterial,
|
|
708
|
-
// IfcClassification, ...) the entity table returns 'Unknown';
|
|
709
|
-
// fall back to ref.type so the schema-driven attribute-name
|
|
710
|
-
// resolution still works for those types.
|
|
711
|
-
const tableName = store.entities.getTypeName(entityId);
|
|
712
|
-
const typeName = tableName && tableName !== 'Unknown' ? tableName : ref.type;
|
|
713
|
-
// Named across the bundled schema union (2X3 + 4 + 4X3), not through the
|
|
714
|
-
// IFC4 codegen pin alone. The pin answers an EMPTY list — not a wrong one —
|
|
715
|
-
// for every class it does not carry, and 251 real classes are outside it:
|
|
716
|
-
// the IFC2X3 ones IFC4 dropped and the whole IFC4X3 infrastructure
|
|
717
|
-
// vocabulary (`IfcCourse`, `IfcPavement`, `IfcKerb`, `IfcSignal`, `IfcRoad`,
|
|
718
|
-
// …). An empty list means this function returns NO attributes for such an
|
|
719
|
-
// entity, so every caller that looks one up by name silently finds nothing
|
|
720
|
-
// and cannot tell that apart from an unset slot. The model-diff adapters
|
|
721
|
-
// read `PredefinedType` this way, which made a cleared `PredefinedType` —
|
|
722
|
-
// the single most common edit in a real infrastructure revision — compare
|
|
723
|
-
// equal to itself on exactly the classes IFC4X3 exists for. Same pinned-
|
|
724
|
-
// registry family as #2001/#2003/#2021.
|
|
725
|
-
//
|
|
726
|
-
// Provably additive: `getAttributeNamesAcrossSchemas` returns the pinned
|
|
727
|
-
// result unchanged whenever the pin has one, so no IFC2X3 or IFC4 entity's
|
|
728
|
-
// attribute list — or the hash anyone derives from it — moves.
|
|
729
|
-
const attrNames = getAttributeNamesAcrossSchemas(typeName);
|
|
730
|
-
const result = [];
|
|
731
|
-
const len = Math.min(attrs.length, attrNames.length);
|
|
732
|
-
for (let i = 0; i < len; i++) {
|
|
733
|
-
const attrName = attrNames[i];
|
|
734
|
-
if (SKIP_DISPLAY_ATTRS.has(attrName))
|
|
735
|
-
continue;
|
|
736
|
-
const raw = attrs[i];
|
|
737
|
-
// STEP `$` (unset) and `*` (derived) deserialize as null /
|
|
738
|
-
// undefined and must be skipped. Strings are emitted with
|
|
739
|
-
// `.ENUM.` markers stripped. Empty strings are preserved so
|
|
740
|
-
// IDS optional-attribute checks can distinguish "slot truly
|
|
741
|
-
// absent" from "slot explicitly empty". Numbers and booleans
|
|
742
|
-
// pass through unchanged so e.g. `CountValue = 0` reads as
|
|
743
|
-
// present.
|
|
744
|
-
if (typeof raw === 'string') {
|
|
745
|
-
// STEP logical-unknown markers (`.U.`, `.X.`) read as
|
|
746
|
-
// "no value" per IDS spec — they fail any attribute
|
|
747
|
-
// check, including a bare existence check, so don't
|
|
748
|
-
// surface them as if the slot were populated.
|
|
749
|
-
if (raw === '.U.' || raw === '.X.')
|
|
750
|
-
continue;
|
|
751
|
-
// Bare boolean tokens (`.T.` / `.F.`) on a schema-typed
|
|
752
|
-
// IfcBoolean attribute slot — resolve to JS boolean so
|
|
753
|
-
// IDS checks comparing against `true` / `false` literals
|
|
754
|
-
// pass without case-sensitive string contortions.
|
|
755
|
-
if (raw === '.T.') {
|
|
756
|
-
result.push({ name: attrName, value: true });
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
if (raw === '.F.') {
|
|
760
|
-
result.push({ name: attrName, value: false });
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
const display = raw.startsWith('.') && raw.endsWith('.')
|
|
764
|
-
? raw.slice(1, -1)
|
|
765
|
-
: raw;
|
|
766
|
-
result.push({ name: attrName, value: display });
|
|
767
|
-
}
|
|
768
|
-
else if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
769
|
-
result.push({ name: attrName, value: raw });
|
|
770
|
-
}
|
|
771
|
-
else if (Array.isArray(raw) && raw.length === 2) {
|
|
772
|
-
// Typed STEP values like IFCREAL(0.0), IFCBOOLEAN(.T.) —
|
|
773
|
-
// return the underlying primitive so attribute existence
|
|
774
|
-
// and value checks can compare it directly.
|
|
775
|
-
const inner = raw[1];
|
|
776
|
-
const tag = String(raw[0]).toUpperCase();
|
|
777
|
-
if (tag.includes('BOOLEAN')) {
|
|
778
|
-
result.push({ name: attrName, value: inner === '.T.' || inner === true });
|
|
779
|
-
}
|
|
780
|
-
else if (tag.includes('LOGICAL')) {
|
|
781
|
-
if (inner === '.U.' || inner === '.X.') {
|
|
782
|
-
// UNKNOWN logical → don't surface (treated as absent)
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
result.push({ name: attrName, value: inner === '.T.' || inner === true });
|
|
786
|
-
}
|
|
787
|
-
else if (typeof inner === 'number' || typeof inner === 'boolean') {
|
|
788
|
-
result.push({ name: attrName, value: inner });
|
|
789
|
-
}
|
|
790
|
-
else if (typeof inner === 'string' && inner) {
|
|
791
|
-
const display = inner.startsWith('.') && inner.endsWith('.')
|
|
792
|
-
? inner.slice(1, -1)
|
|
793
|
-
: inner;
|
|
794
|
-
result.push({ name: attrName, value: display });
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
return result;
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Returns named raw attribute pairs for an entity, filtered to display-relevant attributes.
|
|
802
|
-
* Skips structural/reference attributes using the IFC schema. Used by query layer for coercion.
|
|
803
|
-
*/
|
|
804
|
-
export function getRawNamedAttributes(entity) {
|
|
805
|
-
const attrs = entity.attributes || [];
|
|
806
|
-
const attrNames = getAttributeNames(entity.type);
|
|
807
|
-
const result = [];
|
|
808
|
-
const len = Math.min(attrs.length, attrNames.length);
|
|
809
|
-
for (let i = 0; i < len; i++) {
|
|
810
|
-
const attrName = attrNames[i];
|
|
811
|
-
if (SKIP_DISPLAY_ATTRS.has(attrName))
|
|
812
|
-
continue;
|
|
813
|
-
result.push({ name: attrName, raw: attrs[i] });
|
|
814
|
-
}
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
// getAttributeNames() walks the schema registry (an O(types) scan for the
|
|
818
|
-
// UPPERCASE STEP names entities carry), so memoise the per-type index lookup.
|
|
819
|
-
// There are only a few hundred distinct types but potentially millions of
|
|
820
|
-
// entities, keeping the on-demand path cheap even when called per entity.
|
|
821
|
-
const rootAttrIndexCache = new Map();
|
|
822
|
-
function getRootAttrIndices(type) {
|
|
823
|
-
let idx = rootAttrIndexCache.get(type);
|
|
824
|
-
if (!idx) {
|
|
825
|
-
const names = getAttributeNames(type);
|
|
826
|
-
idx = {
|
|
827
|
-
known: names.length > 0,
|
|
828
|
-
globalId: names.indexOf('GlobalId'),
|
|
829
|
-
name: names.indexOf('Name'),
|
|
830
|
-
description: names.indexOf('Description'),
|
|
831
|
-
objectType: names.indexOf('ObjectType'),
|
|
832
|
-
tag: names.indexOf('Tag'),
|
|
833
|
-
longName: names.indexOf('LongName'),
|
|
834
|
-
};
|
|
835
|
-
rootAttrIndexCache.set(type, idx);
|
|
836
|
-
}
|
|
837
|
-
return idx;
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Resolve the common IfcRoot-family display attributes (GlobalId, Name,
|
|
841
|
-
* Description, ObjectType, Tag) from an entity's raw attribute array.
|
|
842
|
-
*
|
|
843
|
-
* These are mapped by schema-derived attribute *name*, not fixed index. The
|
|
844
|
-
* fixed indices `[0],[2],[3],[4],[7]` only hold for the IfcElement layout: for
|
|
845
|
-
* a spatial element `attrs[7]` is LongName (not Tag), and for a resource entity
|
|
846
|
-
* like IfcMaterial `attrs[0]` is Name (not GlobalId). Name-mapping keeps all of
|
|
847
|
-
* these correct for every entity type, returning '' for attributes the type
|
|
848
|
-
* does not declare.
|
|
849
|
-
*
|
|
850
|
-
* For types the schema registry does not recognise (e.g. an IFC4x3 infra leaf
|
|
851
|
-
* outside the codegen pin, or a vendor extension) we fall back to the canonical
|
|
852
|
-
* IfcRoot/IfcElement positions so we never regress vs. the old fixed-index path.
|
|
853
|
-
*/
|
|
854
|
-
export function extractRootAttributesFromEntity(entity) {
|
|
855
|
-
const attrs = entity.attributes || [];
|
|
856
|
-
const enumIdx = entity.enumAttrIndices;
|
|
857
|
-
const idx = getRootAttrIndices(entity.type);
|
|
858
|
-
const pick = (schemaIndex, fallbackIndex) => {
|
|
859
|
-
const i = idx.known ? schemaIndex : fallbackIndex;
|
|
860
|
-
const raw = i >= 0 ? attrs[i] : undefined;
|
|
861
|
-
if (typeof raw !== 'string')
|
|
862
|
-
return '';
|
|
863
|
-
// Unknown-type fallback only: a fixed index can land on a STEP bare-enum
|
|
864
|
-
// token — e.g. a PredefinedType at attr 7 for a non-IfcElement layout —
|
|
865
|
-
// which must not leak into a Tag/Description cell. Reject by token KIND
|
|
866
|
-
// via the extractor's side channel (#1799), not by dotted-string shape:
|
|
867
|
-
// a quoted string that merely looks like an enum ('.USERDEFINED.')
|
|
868
|
-
// survives, exactly matching the Rust server path (string_at accepts
|
|
869
|
-
// AttributeValue::String and rejects ::Enum, #1779). Skipped for known
|
|
870
|
-
// types, whose schema indices point at genuine string slots.
|
|
871
|
-
if (!idx.known && enumIdx !== undefined && enumIdx.includes(i))
|
|
872
|
-
return '';
|
|
873
|
-
return raw;
|
|
874
|
-
};
|
|
875
|
-
return {
|
|
876
|
-
globalId: pick(idx.globalId, 0),
|
|
877
|
-
name: pick(idx.name, 2),
|
|
878
|
-
description: pick(idx.description, 3),
|
|
879
|
-
objectType: pick(idx.objectType, 4),
|
|
880
|
-
tag: pick(idx.tag, 7),
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Resolve an entity's LongName (IfcZone, IfcSystem subtypes, IfcSpatialZone,
|
|
885
|
-
* IfcBuildingSystem, …) by schema attribute name. Groups frequently leave Name
|
|
886
|
-
* empty and carry their human label in LongName, so consumers fall back to this
|
|
887
|
-
* for the display label. Returns '' when the type does not declare LongName.
|
|
888
|
-
* (#1075)
|
|
889
|
-
*/
|
|
890
|
-
export function pickLongName(entity) {
|
|
891
|
-
const idx = getRootAttrIndices(entity.type);
|
|
892
|
-
if (!idx.known || idx.longName < 0)
|
|
893
|
-
return '';
|
|
894
|
-
const raw = (entity.attributes || [])[idx.longName];
|
|
895
|
-
return typeof raw === 'string' ? raw : '';
|
|
896
|
-
}
|
|
897
518
|
// Re-export on-demand extraction functions from focused module
|
|
898
519
|
export { extractClassificationsOnDemand, extractClassificationSystemsOnDemand, extractMaterialsOnDemand, extractAllMaterialsOnDemand, extractMaterialPropertiesOnDemand, extractMaterialPropertiesForMaterialId, resolveMaterialDefId, resolveAllMaterialDefIds, collectMaterialLeaves, buildMaterialUsageIndex, getMaterialDisplay, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractTypeQuantitiesOnDemand, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGroupMembersOnDemand, extractGeoreferencingOnDemand, parsePropertyValue, extractPsetsFromIds, extractQsetsFromIds, } from './on-demand-extractors.js';
|
|
899
520
|
export { mergeInheritedPropertySets, mergeInheritedQuantitySets } from './property-set-merge.js';
|