@ifc-lite/parser 2.1.1 → 2.1.3
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/columnar-parser.d.ts +1 -4
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +568 -253
- package/dist/columnar-parser.js.map +1 -1
- package/dist/compact-entity-index.d.ts.map +1 -1
- package/dist/compact-entity-index.js +12 -5
- package/dist/compact-entity-index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -13
- package/dist/index.js.map +1 -1
- package/dist/scan-worker-inline.d.ts +21 -0
- package/dist/scan-worker-inline.d.ts.map +1 -0
- package/dist/scan-worker-inline.js +239 -0
- package/dist/scan-worker-inline.js.map +1 -0
- package/package.json +2 -2
package/dist/columnar-parser.js
CHANGED
|
@@ -62,10 +62,16 @@ const QUANTITY_TYPE_MAP = {
|
|
|
62
62
|
const SPATIAL_TYPES = new Set([
|
|
63
63
|
'IFCPROJECT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY', 'IFCSPACE',
|
|
64
64
|
]);
|
|
65
|
-
// Relationship types needed for hierarchy
|
|
65
|
+
// Relationship types needed for hierarchy and structural relationships
|
|
66
66
|
const HIERARCHY_REL_TYPES = new Set([
|
|
67
67
|
'IFCRELAGGREGATES', 'IFCRELCONTAINEDINSPATIALSTRUCTURE',
|
|
68
68
|
'IFCRELDEFINESBYTYPE',
|
|
69
|
+
// Structural relationships (voids, fills, connections, groups)
|
|
70
|
+
'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
|
|
71
|
+
'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
|
|
72
|
+
'IFCRELSPACEBOUNDARY',
|
|
73
|
+
'IFCRELASSIGNSTOGROUP', 'IFCRELASSIGNSTOPRODUCT',
|
|
74
|
+
'IFCRELREFERENCEDINSPATIALSTRUCTURE',
|
|
69
75
|
]);
|
|
70
76
|
// Relationship types for on-demand property loading
|
|
71
77
|
const PROPERTY_REL_TYPES = new Set([
|
|
@@ -90,10 +96,303 @@ const PROPERTY_ENTITY_TYPES = new Set([
|
|
|
90
96
|
function isIfcTypeLikeEntity(typeUpper) {
|
|
91
97
|
return typeUpper.endsWith('TYPE') || typeUpper.endsWith('STYLE');
|
|
92
98
|
}
|
|
99
|
+
// ==========================================
|
|
100
|
+
// Byte-level helpers for fast extraction
|
|
101
|
+
// These avoid per-entity TextDecoder calls by working on raw bytes.
|
|
102
|
+
// ==========================================
|
|
93
103
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
104
|
+
* Find the byte range of a quoted string at a specific attribute position in STEP entity bytes.
|
|
105
|
+
* Returns [start, end) byte offsets (excluding quotes), or null if not found.
|
|
106
|
+
*
|
|
107
|
+
* @param buffer - The IFC file buffer
|
|
108
|
+
* @param entityStart - byte offset of the entity
|
|
109
|
+
* @param entityLen - byte length of the entity
|
|
110
|
+
* @param attrIndex - 0-based attribute index (0=GlobalId, 2=Name)
|
|
96
111
|
*/
|
|
112
|
+
function findQuotedAttrRange(buffer, entityStart, entityLen, attrIndex) {
|
|
113
|
+
const end = entityStart + entityLen;
|
|
114
|
+
let pos = entityStart;
|
|
115
|
+
// Skip to opening paren '(' after TYPE name
|
|
116
|
+
while (pos < end && buffer[pos] !== 0x28 /* ( */)
|
|
117
|
+
pos++;
|
|
118
|
+
if (pos >= end)
|
|
119
|
+
return null;
|
|
120
|
+
pos++; // skip '('
|
|
121
|
+
// Skip commas to reach the target attribute
|
|
122
|
+
if (attrIndex > 0) {
|
|
123
|
+
let toSkip = attrIndex;
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let inStr = false;
|
|
126
|
+
while (pos < end && toSkip > 0) {
|
|
127
|
+
const ch = buffer[pos];
|
|
128
|
+
if (ch === 0x27 /* ' */) {
|
|
129
|
+
if (inStr && pos + 1 < end && buffer[pos + 1] === 0x27) {
|
|
130
|
+
pos += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
inStr = !inStr;
|
|
134
|
+
}
|
|
135
|
+
else if (!inStr) {
|
|
136
|
+
if (ch === 0x28)
|
|
137
|
+
depth++;
|
|
138
|
+
else if (ch === 0x29)
|
|
139
|
+
depth--;
|
|
140
|
+
else if (ch === 0x2C && depth === 0)
|
|
141
|
+
toSkip--;
|
|
142
|
+
}
|
|
143
|
+
pos++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Skip whitespace
|
|
147
|
+
while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
|
|
148
|
+
pos++;
|
|
149
|
+
// Check for quoted string
|
|
150
|
+
if (pos >= end || buffer[pos] !== 0x27 /* ' */)
|
|
151
|
+
return null;
|
|
152
|
+
pos++; // skip opening quote
|
|
153
|
+
const start = pos;
|
|
154
|
+
// Find closing quote (handle escaped quotes '')
|
|
155
|
+
while (pos < end) {
|
|
156
|
+
if (buffer[pos] === 0x27) {
|
|
157
|
+
if (pos + 1 < end && buffer[pos + 1] === 0x27) {
|
|
158
|
+
pos += 2;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
pos++;
|
|
164
|
+
}
|
|
165
|
+
return [start, pos];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Batch extract GlobalId (attr[0]) and Name (attr[2]) for many entities using
|
|
169
|
+
* only 2 TextDecoder.decode() calls total (one for all GlobalIds, one for all Names).
|
|
170
|
+
*
|
|
171
|
+
* This is ~100x faster than calling extractEntity() per entity for large batches
|
|
172
|
+
* because it eliminates per-entity TextDecoder overhead which is significant in Firefox.
|
|
173
|
+
*
|
|
174
|
+
* Returns a Map from expressId → { globalId, name }.
|
|
175
|
+
*/
|
|
176
|
+
function batchExtractGlobalIdAndName(buffer, refs) {
|
|
177
|
+
const result = new Map();
|
|
178
|
+
if (refs.length === 0)
|
|
179
|
+
return result;
|
|
180
|
+
// Phase 1: Scan byte ranges for GlobalId and Name positions (no string allocation)
|
|
181
|
+
const gidRanges = []; // [start, end) for each entity
|
|
182
|
+
const nameRanges = [];
|
|
183
|
+
const validIndices = []; // indices into refs for entities with valid ranges
|
|
184
|
+
for (let i = 0; i < refs.length; i++) {
|
|
185
|
+
const ref = refs[i];
|
|
186
|
+
const gidRange = findQuotedAttrRange(buffer, ref.byteOffset, ref.byteLength, 0);
|
|
187
|
+
const nameRange = findQuotedAttrRange(buffer, ref.byteOffset, ref.byteLength, 2);
|
|
188
|
+
gidRanges.push(gidRange ?? [0, 0]);
|
|
189
|
+
nameRanges.push(nameRange ?? [0, 0]);
|
|
190
|
+
validIndices.push(i);
|
|
191
|
+
}
|
|
192
|
+
// Phase 2: Concatenate all GlobalId bytes into one buffer, decode once
|
|
193
|
+
// Use null byte (0x00) as separator (never appears in IFC string content)
|
|
194
|
+
let totalGidBytes = 0;
|
|
195
|
+
let totalNameBytes = 0;
|
|
196
|
+
for (let i = 0; i < validIndices.length; i++) {
|
|
197
|
+
const [gs, ge] = gidRanges[i];
|
|
198
|
+
const [ns, ne] = nameRanges[i];
|
|
199
|
+
totalGidBytes += (ge - gs) + 1; // +1 for separator
|
|
200
|
+
totalNameBytes += (ne - ns) + 1;
|
|
201
|
+
}
|
|
202
|
+
const gidBuf = new Uint8Array(totalGidBytes);
|
|
203
|
+
const nameBuf = new Uint8Array(totalNameBytes);
|
|
204
|
+
let gidOffset = 0;
|
|
205
|
+
let nameOffset = 0;
|
|
206
|
+
for (let i = 0; i < validIndices.length; i++) {
|
|
207
|
+
const [gs, ge] = gidRanges[i];
|
|
208
|
+
const [ns, ne] = nameRanges[i];
|
|
209
|
+
if (ge > gs) {
|
|
210
|
+
gidBuf.set(buffer.subarray(gs, ge), gidOffset);
|
|
211
|
+
gidOffset += ge - gs;
|
|
212
|
+
}
|
|
213
|
+
gidBuf[gidOffset++] = 0; // null separator
|
|
214
|
+
if (ne > ns) {
|
|
215
|
+
nameBuf.set(buffer.subarray(ns, ne), nameOffset);
|
|
216
|
+
nameOffset += ne - ns;
|
|
217
|
+
}
|
|
218
|
+
nameBuf[nameOffset++] = 0;
|
|
219
|
+
}
|
|
220
|
+
// Phase 3: Two TextDecoder calls for ALL entities
|
|
221
|
+
const decoder = new TextDecoder();
|
|
222
|
+
const allGids = decoder.decode(gidBuf.subarray(0, gidOffset));
|
|
223
|
+
const allNames = decoder.decode(nameBuf.subarray(0, nameOffset));
|
|
224
|
+
const gids = allGids.split('\0');
|
|
225
|
+
const names = allNames.split('\0');
|
|
226
|
+
// Phase 4: Build result map
|
|
227
|
+
for (let i = 0; i < validIndices.length; i++) {
|
|
228
|
+
const ref = refs[validIndices[i]];
|
|
229
|
+
result.set(ref.expressId, {
|
|
230
|
+
globalId: gids[i] || '',
|
|
231
|
+
name: names[i] || '',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
// ==========================================
|
|
237
|
+
// Byte-level relationship scanners (numbers only, no TextDecoder)
|
|
238
|
+
// ==========================================
|
|
239
|
+
/**
|
|
240
|
+
* Skip N commas at depth 0 in STEP bytes.
|
|
241
|
+
*/
|
|
242
|
+
function skipCommas(buffer, start, end, count) {
|
|
243
|
+
let pos = start;
|
|
244
|
+
let remaining = count;
|
|
245
|
+
let depth = 0;
|
|
246
|
+
let inString = false;
|
|
247
|
+
while (pos < end && remaining > 0) {
|
|
248
|
+
const ch = buffer[pos];
|
|
249
|
+
if (ch === 0x27) {
|
|
250
|
+
if (inString && pos + 1 < end && buffer[pos + 1] === 0x27) {
|
|
251
|
+
pos += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
inString = !inString;
|
|
255
|
+
}
|
|
256
|
+
else if (!inString) {
|
|
257
|
+
if (ch === 0x28)
|
|
258
|
+
depth++;
|
|
259
|
+
else if (ch === 0x29)
|
|
260
|
+
depth--;
|
|
261
|
+
else if (ch === 0x2C && depth === 0)
|
|
262
|
+
remaining--;
|
|
263
|
+
}
|
|
264
|
+
pos++;
|
|
265
|
+
}
|
|
266
|
+
return pos;
|
|
267
|
+
}
|
|
268
|
+
/** Read a #ID entity reference as a number. Returns -1 if not an entity ref. */
|
|
269
|
+
function readRefId(buffer, pos, end) {
|
|
270
|
+
while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
|
|
271
|
+
pos++;
|
|
272
|
+
if (pos < end && buffer[pos] === 0x23) {
|
|
273
|
+
pos++;
|
|
274
|
+
let num = 0;
|
|
275
|
+
while (pos < end && buffer[pos] >= 0x30 && buffer[pos] <= 0x39) {
|
|
276
|
+
num = num * 10 + (buffer[pos] - 0x30);
|
|
277
|
+
pos++;
|
|
278
|
+
}
|
|
279
|
+
return [num, pos];
|
|
280
|
+
}
|
|
281
|
+
return [-1, pos];
|
|
282
|
+
}
|
|
283
|
+
/** Read a list of entity refs (#id1,#id2,...) or a single #id. Returns [ids, newPos]. */
|
|
284
|
+
function readRefList(buffer, pos, end) {
|
|
285
|
+
while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
|
|
286
|
+
pos++;
|
|
287
|
+
const ids = [];
|
|
288
|
+
if (pos < end && buffer[pos] === 0x28) {
|
|
289
|
+
pos++;
|
|
290
|
+
while (pos < end && buffer[pos] !== 0x29) {
|
|
291
|
+
while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09 || buffer[pos] === 0x2C))
|
|
292
|
+
pos++;
|
|
293
|
+
if (pos < end && buffer[pos] === 0x23) {
|
|
294
|
+
const [id, np] = readRefId(buffer, pos, end);
|
|
295
|
+
if (id >= 0)
|
|
296
|
+
ids.push(id);
|
|
297
|
+
pos = np;
|
|
298
|
+
}
|
|
299
|
+
else if (pos < end && buffer[pos] !== 0x29) {
|
|
300
|
+
pos++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (pos < end && buffer[pos] === 0x23) {
|
|
305
|
+
const [id, np] = readRefId(buffer, pos, end);
|
|
306
|
+
if (id >= 0)
|
|
307
|
+
ids.push(id);
|
|
308
|
+
pos = np;
|
|
309
|
+
}
|
|
310
|
+
return [ids, pos];
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Extract relatingObject and relatedObjects from a relationship entity using byte-level scanning.
|
|
314
|
+
* No TextDecoder needed - only extracts numeric entity IDs.
|
|
315
|
+
*/
|
|
316
|
+
function extractRelFast(buffer, byteOffset, byteLength, typeUpper) {
|
|
317
|
+
const end = byteOffset + byteLength;
|
|
318
|
+
let pos = byteOffset;
|
|
319
|
+
while (pos < end && buffer[pos] !== 0x28)
|
|
320
|
+
pos++;
|
|
321
|
+
if (pos >= end)
|
|
322
|
+
return null;
|
|
323
|
+
pos++;
|
|
324
|
+
// Skip to attr[4] (all IfcRelationship subtypes have 4 shared IfcRoot+IfcRelationship attrs)
|
|
325
|
+
pos = skipCommas(buffer, pos, end, 4);
|
|
326
|
+
if (typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE'
|
|
327
|
+
|| typeUpper === 'IFCRELREFERENCEDINSPATIALSTRUCTURE'
|
|
328
|
+
|| typeUpper === 'IFCRELDEFINESBYPROPERTIES'
|
|
329
|
+
|| typeUpper === 'IFCRELDEFINESBYTYPE') {
|
|
330
|
+
// attr[4]=RelatedObjects, attr[5]=RelatingObject
|
|
331
|
+
const [related, rp] = readRefList(buffer, pos, end);
|
|
332
|
+
pos = rp;
|
|
333
|
+
while (pos < end && buffer[pos] !== 0x2C)
|
|
334
|
+
pos++;
|
|
335
|
+
pos++;
|
|
336
|
+
const [relating, _] = readRefId(buffer, pos, end);
|
|
337
|
+
if (relating < 0 || related.length === 0)
|
|
338
|
+
return null;
|
|
339
|
+
return { relatingObject: relating, relatedObjects: related };
|
|
340
|
+
}
|
|
341
|
+
else if (typeUpper === 'IFCRELASSIGNSTOGROUP' || typeUpper === 'IFCRELASSIGNSTOPRODUCT') {
|
|
342
|
+
const [related, rp] = readRefList(buffer, pos, end);
|
|
343
|
+
pos = skipCommas(buffer, rp, end, 2);
|
|
344
|
+
const [relating, _] = readRefId(buffer, pos, end);
|
|
345
|
+
if (relating < 0 || related.length === 0)
|
|
346
|
+
return null;
|
|
347
|
+
return { relatingObject: relating, relatedObjects: related };
|
|
348
|
+
}
|
|
349
|
+
else if (typeUpper === 'IFCRELCONNECTSELEMENTS' || typeUpper === 'IFCRELCONNECTSPATHELEMENTS') {
|
|
350
|
+
pos = skipCommas(buffer, pos, end, 1);
|
|
351
|
+
const [relating, rp2] = readRefId(buffer, pos, end);
|
|
352
|
+
pos = skipCommas(buffer, rp2, end, 1);
|
|
353
|
+
const [related, _] = readRefId(buffer, pos, end);
|
|
354
|
+
if (relating < 0 || related < 0)
|
|
355
|
+
return null;
|
|
356
|
+
return { relatingObject: relating, relatedObjects: [related] };
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Default: attr[4]=RelatingObject, attr[5]=RelatedObject(s)
|
|
360
|
+
const [relating, rp] = readRefId(buffer, pos, end);
|
|
361
|
+
if (relating < 0)
|
|
362
|
+
return null;
|
|
363
|
+
pos = rp;
|
|
364
|
+
while (pos < end && buffer[pos] !== 0x2C)
|
|
365
|
+
pos++;
|
|
366
|
+
pos++;
|
|
367
|
+
const [related, _] = readRefList(buffer, pos, end);
|
|
368
|
+
if (related.length === 0)
|
|
369
|
+
return null;
|
|
370
|
+
return { relatingObject: relating, relatedObjects: related };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Extract property rel data: attr[4]=relatedObjects, attr[5]=relatingDef.
|
|
375
|
+
* Numbers only, no TextDecoder.
|
|
376
|
+
*/
|
|
377
|
+
function extractPropertyRelFast(buffer, byteOffset, byteLength) {
|
|
378
|
+
const end = byteOffset + byteLength;
|
|
379
|
+
let pos = byteOffset;
|
|
380
|
+
while (pos < end && buffer[pos] !== 0x28)
|
|
381
|
+
pos++;
|
|
382
|
+
if (pos >= end)
|
|
383
|
+
return null;
|
|
384
|
+
pos++;
|
|
385
|
+
pos = skipCommas(buffer, pos, end, 4);
|
|
386
|
+
const [relatedObjects, rp] = readRefList(buffer, pos, end);
|
|
387
|
+
pos = rp;
|
|
388
|
+
while (pos < end && buffer[pos] !== 0x2C)
|
|
389
|
+
pos++;
|
|
390
|
+
pos++;
|
|
391
|
+
const [relatingDef, _] = readRefId(buffer, pos, end);
|
|
392
|
+
if (relatingDef < 0 || relatedObjects.length === 0)
|
|
393
|
+
return null;
|
|
394
|
+
return { relatedObjects, relatingDef };
|
|
395
|
+
}
|
|
97
396
|
function detectSchemaVersion(buffer) {
|
|
98
397
|
const headerEnd = Math.min(buffer.length, 2000);
|
|
99
398
|
const headerText = new TextDecoder().decode(buffer.subarray(0, headerEnd)).toUpperCase();
|
|
@@ -119,32 +418,67 @@ export class ColumnarParser {
|
|
|
119
418
|
const startTime = performance.now();
|
|
120
419
|
const uint8Buffer = new Uint8Array(buffer);
|
|
121
420
|
const totalEntities = entityRefs.length;
|
|
421
|
+
// Phase timing for performance telemetry
|
|
422
|
+
let phaseStart = startTime;
|
|
423
|
+
const logPhase = (name) => {
|
|
424
|
+
const now = performance.now();
|
|
425
|
+
console.log(`[parseLite] ${name}: ${Math.round(now - phaseStart)}ms`);
|
|
426
|
+
phaseStart = now;
|
|
427
|
+
};
|
|
122
428
|
options.onProgress?.({ phase: 'building', percent: 0 });
|
|
123
429
|
// Detect schema version from FILE_SCHEMA header
|
|
124
430
|
const schemaVersion = detectSchemaVersion(uint8Buffer);
|
|
125
|
-
// Initialize builders
|
|
431
|
+
// Initialize builders (entity table capacity set after categorization below)
|
|
126
432
|
const strings = new StringTable();
|
|
127
|
-
const entityTableBuilder = new EntityTableBuilder(totalEntities, strings);
|
|
128
433
|
const propertyTableBuilder = new PropertyTableBuilder(strings);
|
|
129
434
|
const quantityTableBuilder = new QuantityTableBuilder(strings);
|
|
130
435
|
const relationshipGraphBuilder = new RelationshipGraphBuilder();
|
|
436
|
+
logPhase('init builders');
|
|
131
437
|
// Build compact entity index (typed arrays instead of Map for ~3x memory reduction)
|
|
132
438
|
const compactByIdIndex = buildCompactEntityIndex(entityRefs);
|
|
133
|
-
|
|
439
|
+
logPhase('compact entity index');
|
|
440
|
+
// Single pass: build byType index AND categorize entities simultaneously.
|
|
441
|
+
// Uses a type-name cache to avoid calling .toUpperCase() on 4.4M refs
|
|
442
|
+
// (only ~776 unique type names in IFC4).
|
|
134
443
|
const byType = new Map();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
444
|
+
const RELEVANT_ENTITY_PREFIXES = new Set([
|
|
445
|
+
'IFCWALL', 'IFCSLAB', 'IFCBEAM', 'IFCCOLUMN', 'IFCPLATE', 'IFCDOOR', 'IFCWINDOW',
|
|
446
|
+
'IFCROOF', 'IFCSTAIR', 'IFCRAILING', 'IFCRAMP', 'IFCFOOTING', 'IFCPILE',
|
|
447
|
+
'IFCMEMBER', 'IFCCURTAINWALL', 'IFCBUILDINGELEMENTPROXY', 'IFCFURNISHINGELEMENT',
|
|
448
|
+
'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL', 'IFCFLOWCONTROLLER', 'IFCFLOWFITTING',
|
|
449
|
+
'IFCSPACE', 'IFCOPENINGELEMENT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
|
|
450
|
+
'IFCPROJECT', 'IFCCOVERING', 'IFCANNOTATION', 'IFCGRID',
|
|
451
|
+
]);
|
|
452
|
+
// Category constants for the lookup cache
|
|
453
|
+
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;
|
|
454
|
+
// Cache: type name → category (avoids 4.4M .toUpperCase() calls)
|
|
455
|
+
const typeCategoryCache = new Map();
|
|
456
|
+
function getCategory(type) {
|
|
457
|
+
let cat = typeCategoryCache.get(type);
|
|
458
|
+
if (cat !== undefined)
|
|
459
|
+
return cat;
|
|
460
|
+
const upper = type.toUpperCase();
|
|
461
|
+
if (SPATIAL_TYPES.has(upper))
|
|
462
|
+
cat = CAT_SPATIAL;
|
|
463
|
+
else if (GEOMETRY_TYPES.has(upper))
|
|
464
|
+
cat = CAT_GEOMETRY;
|
|
465
|
+
else if (HIERARCHY_REL_TYPES.has(upper))
|
|
466
|
+
cat = CAT_HIERARCHY_REL;
|
|
467
|
+
else if (PROPERTY_REL_TYPES.has(upper))
|
|
468
|
+
cat = CAT_PROPERTY_REL;
|
|
469
|
+
else if (PROPERTY_ENTITY_TYPES.has(upper))
|
|
470
|
+
cat = CAT_PROPERTY_ENTITY;
|
|
471
|
+
else if (ASSOCIATION_REL_TYPES.has(upper))
|
|
472
|
+
cat = CAT_ASSOCIATION_REL;
|
|
473
|
+
else if (isIfcTypeLikeEntity(upper))
|
|
474
|
+
cat = CAT_TYPE_OBJECT;
|
|
475
|
+
else if (RELEVANT_ENTITY_PREFIXES.has(upper) || upper.startsWith('IFCREL'))
|
|
476
|
+
cat = CAT_RELEVANT;
|
|
477
|
+
else
|
|
478
|
+
cat = CAT_SKIP;
|
|
479
|
+
typeCategoryCache.set(type, cat);
|
|
480
|
+
return cat;
|
|
142
481
|
}
|
|
143
|
-
const entityIndex = {
|
|
144
|
-
byId: compactByIdIndex,
|
|
145
|
-
byType,
|
|
146
|
-
};
|
|
147
|
-
// First pass: collect spatial, geometry, relationship, property, and type refs for targeted parsing
|
|
148
482
|
const spatialRefs = [];
|
|
149
483
|
const geometryRefs = [];
|
|
150
484
|
const relationshipRefs = [];
|
|
@@ -152,238 +486,142 @@ export class ColumnarParser {
|
|
|
152
486
|
const propertyEntityRefs = [];
|
|
153
487
|
const associationRelRefs = [];
|
|
154
488
|
const typeObjectRefs = [];
|
|
489
|
+
const otherRelevantRefs = [];
|
|
155
490
|
for (const ref of entityRefs) {
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
491
|
+
// Build byType index
|
|
492
|
+
let typeList = byType.get(ref.type);
|
|
493
|
+
if (!typeList) {
|
|
494
|
+
typeList = [];
|
|
495
|
+
byType.set(ref.type, typeList);
|
|
160
496
|
}
|
|
161
|
-
|
|
497
|
+
typeList.push(ref.expressId);
|
|
498
|
+
// Categorize (cached — .toUpperCase() called once per unique type)
|
|
499
|
+
const cat = getCategory(ref.type);
|
|
500
|
+
if (cat === CAT_SPATIAL)
|
|
501
|
+
spatialRefs.push(ref);
|
|
502
|
+
else if (cat === CAT_GEOMETRY)
|
|
162
503
|
geometryRefs.push(ref);
|
|
163
|
-
|
|
164
|
-
else if (HIERARCHY_REL_TYPES.has(typeUpper)) {
|
|
504
|
+
else if (cat === CAT_HIERARCHY_REL)
|
|
165
505
|
relationshipRefs.push(ref);
|
|
166
|
-
|
|
167
|
-
else if (PROPERTY_REL_TYPES.has(typeUpper)) {
|
|
506
|
+
else if (cat === CAT_PROPERTY_REL)
|
|
168
507
|
propertyRelRefs.push(ref);
|
|
169
|
-
|
|
170
|
-
else if (PROPERTY_ENTITY_TYPES.has(typeUpper)) {
|
|
508
|
+
else if (cat === CAT_PROPERTY_ENTITY)
|
|
171
509
|
propertyEntityRefs.push(ref);
|
|
172
|
-
|
|
173
|
-
else if (ASSOCIATION_REL_TYPES.has(typeUpper)) {
|
|
510
|
+
else if (cat === CAT_ASSOCIATION_REL)
|
|
174
511
|
associationRelRefs.push(ref);
|
|
175
|
-
|
|
176
|
-
else if (isIfcTypeLikeEntity(typeUpper)) {
|
|
512
|
+
else if (cat === CAT_TYPE_OBJECT)
|
|
177
513
|
typeObjectRefs.push(ref);
|
|
178
|
-
|
|
514
|
+
else if (cat === CAT_RELEVANT)
|
|
515
|
+
otherRelevantRefs.push(ref);
|
|
179
516
|
}
|
|
180
|
-
|
|
181
|
-
|
|
517
|
+
logPhase(`categorize ${totalEntities} → spatial:${spatialRefs.length} geom:${geometryRefs.length} rel:${relationshipRefs.length} propRel:${propertyRelRefs.length} assocRel:${associationRelRefs.length} type:${typeObjectRefs.length} other:${otherRelevantRefs.length}`);
|
|
518
|
+
// Create entity table builder with EXACT capacity (not totalEntities which
|
|
519
|
+
// includes millions of geometry-representation entities we don't store).
|
|
520
|
+
// For a 14M entity file, this reduces allocation from ~546MB to ~20MB.
|
|
521
|
+
const relevantCount = spatialRefs.length + geometryRefs.length + typeObjectRefs.length
|
|
522
|
+
+ relationshipRefs.length + otherRelevantRefs.length;
|
|
523
|
+
const entityTableBuilder = new EntityTableBuilder(relevantCount, strings);
|
|
524
|
+
const entityIndex = {
|
|
525
|
+
byId: compactByIdIndex,
|
|
526
|
+
byType,
|
|
527
|
+
};
|
|
528
|
+
// Time-based yielding: yield to the main thread every ~80ms so geometry
|
|
529
|
+
// streaming callbacks can fire. This limits main-thread blocking to short
|
|
530
|
+
// bursts that don't starve geometry, while adding minimal overhead (~15 yields
|
|
531
|
+
// × ~1ms each ≈ 15ms total over the full parse).
|
|
532
|
+
const YIELD_INTERVAL_MS = 80;
|
|
533
|
+
let lastYieldTime = performance.now();
|
|
534
|
+
const yieldIfNeeded = async () => {
|
|
535
|
+
const now = performance.now();
|
|
536
|
+
if (now - lastYieldTime >= YIELD_INTERVAL_MS) {
|
|
537
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
538
|
+
lastYieldTime = performance.now();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
// === TARGETED PARSING using batch byte-level extraction ===
|
|
542
|
+
// Uses 2 TextDecoder.decode() calls total for ALL entity GlobalIds/Names
|
|
543
|
+
// (instead of per-entity calls), and pure byte scanning for relationships.
|
|
544
|
+
options.onProgress?.({ phase: 'parsing entities', percent: 10 });
|
|
182
545
|
const extractor = new EntityExtractor(uint8Buffer);
|
|
546
|
+
// Spatial entities: small count, use extractEntity for full accuracy
|
|
183
547
|
const parsedEntityData = new Map();
|
|
184
|
-
// Parse spatial entities (typically < 100 entities)
|
|
185
548
|
for (const ref of spatialRefs) {
|
|
186
549
|
const entity = extractor.extractEntity(ref);
|
|
187
550
|
if (entity) {
|
|
188
551
|
const attrs = entity.attributes || [];
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
else {
|
|
194
|
-
console.warn(`[ColumnarParser] Failed to extract spatial entity #${ref.expressId} (${ref.type})`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Parse geometry entities for GlobalIds (needed for BCF component references)
|
|
198
|
-
// IFC entities with geometry have GlobalId at attribute[0] and Name at attribute[2]
|
|
199
|
-
options.onProgress?.({ phase: 'parsing geometry globalIds', percent: 12 });
|
|
200
|
-
for (const ref of geometryRefs) {
|
|
201
|
-
const entity = extractor.extractEntity(ref);
|
|
202
|
-
if (entity) {
|
|
203
|
-
const attrs = entity.attributes || [];
|
|
204
|
-
const globalId = typeof attrs[0] === 'string' ? attrs[0] : '';
|
|
205
|
-
const name = typeof attrs[2] === 'string' ? attrs[2] : '';
|
|
206
|
-
parsedEntityData.set(ref.expressId, { globalId, name });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Parse type objects (IfcWallType, IfcDoorType, etc.) for GlobalId and Name
|
|
210
|
-
// Type objects derive from IfcRoot: attrs[0]=GlobalId, attrs[2]=Name
|
|
211
|
-
// Needed for IDS validation against type entities
|
|
212
|
-
for (const ref of typeObjectRefs) {
|
|
213
|
-
const entity = extractor.extractEntity(ref);
|
|
214
|
-
if (entity) {
|
|
215
|
-
const attrs = entity.attributes || [];
|
|
216
|
-
const globalId = typeof attrs[0] === 'string' ? attrs[0] : '';
|
|
217
|
-
const name = typeof attrs[2] === 'string' ? attrs[2] : '';
|
|
218
|
-
parsedEntityData.set(ref.expressId, { globalId, name });
|
|
552
|
+
parsedEntityData.set(ref.expressId, {
|
|
553
|
+
globalId: typeof attrs[0] === 'string' ? attrs[0] : '',
|
|
554
|
+
name: typeof attrs[2] === 'string' ? attrs[2] : '',
|
|
555
|
+
});
|
|
219
556
|
}
|
|
220
557
|
}
|
|
221
|
-
|
|
558
|
+
logPhase('spatial entities');
|
|
559
|
+
await yieldIfNeeded();
|
|
560
|
+
// Geometry + type object entities: batch extract GlobalId+Name with 2 TextDecoder calls
|
|
561
|
+
options.onProgress?.({ phase: 'parsing geometry names', percent: 12 });
|
|
562
|
+
const geomData = batchExtractGlobalIdAndName(uint8Buffer, geometryRefs);
|
|
563
|
+
for (const [id, data] of geomData)
|
|
564
|
+
parsedEntityData.set(id, data);
|
|
565
|
+
await yieldIfNeeded();
|
|
566
|
+
const typeData = batchExtractGlobalIdAndName(uint8Buffer, typeObjectRefs);
|
|
567
|
+
for (const [id, data] of typeData)
|
|
568
|
+
parsedEntityData.set(id, data);
|
|
569
|
+
logPhase('batch geom GlobalId+Name');
|
|
570
|
+
await yieldIfNeeded();
|
|
571
|
+
// Relationships: byte-level scanning (numbers only, no TextDecoder)
|
|
222
572
|
options.onProgress?.({ phase: 'parsing relationships', percent: 20 });
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
relationships.push(rel);
|
|
231
|
-
// Add to relationship graph
|
|
232
|
-
const relType = REL_TYPE_MAP[typeUpper];
|
|
233
|
-
if (relType) {
|
|
234
|
-
for (const targetId of rel.relatedObjects) {
|
|
235
|
-
relationshipGraphBuilder.addEdge(rel.relatingObject, targetId, relType, rel.relatingObject);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// === PARSE PROPERTY RELATIONSHIPS for on-demand loading ===
|
|
242
|
-
options.onProgress?.({ phase: 'parsing property refs', percent: 25 });
|
|
243
|
-
const onDemandPropertyMap = new Map();
|
|
244
|
-
const onDemandQuantityMap = new Map();
|
|
245
|
-
// Parse IfcRelDefinesByProperties to build entity -> pset/qset mapping
|
|
246
|
-
// ALSO add to relationship graph so cache loads can rebuild on-demand maps
|
|
247
|
-
for (const ref of propertyRelRefs) {
|
|
248
|
-
const entity = extractor.extractEntity(ref);
|
|
249
|
-
if (entity) {
|
|
250
|
-
const attrs = entity.attributes || [];
|
|
251
|
-
// IfcRelDefinesByProperties: relatedObjects at [4], relatingPropertyDefinition at [5]
|
|
252
|
-
const relatedObjects = attrs[4];
|
|
253
|
-
const relatingDef = attrs[5];
|
|
254
|
-
if (typeof relatingDef === 'number' && Array.isArray(relatedObjects)) {
|
|
255
|
-
// Add to relationship graph (needed for cache rebuild)
|
|
256
|
-
for (const objId of relatedObjects) {
|
|
257
|
-
if (typeof objId === 'number') {
|
|
258
|
-
relationshipGraphBuilder.addEdge(relatingDef, objId, RelationshipType.DefinesByProperties, ref.expressId);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Find if the relating definition is a property set or quantity set
|
|
262
|
-
const defRef = entityIndex.byId.get(relatingDef);
|
|
263
|
-
if (defRef) {
|
|
264
|
-
const defTypeUpper = defRef.type.toUpperCase();
|
|
265
|
-
const isPropertySet = defTypeUpper === 'IFCPROPERTYSET';
|
|
266
|
-
const isQuantitySet = defTypeUpper === 'IFCELEMENTQUANTITY';
|
|
267
|
-
if (isPropertySet || isQuantitySet) {
|
|
268
|
-
const targetMap = isPropertySet ? onDemandPropertyMap : onDemandQuantityMap;
|
|
269
|
-
for (const objId of relatedObjects) {
|
|
270
|
-
if (typeof objId === 'number') {
|
|
271
|
-
let list = targetMap.get(objId);
|
|
272
|
-
if (!list) {
|
|
273
|
-
list = [];
|
|
274
|
-
targetMap.set(objId, list);
|
|
275
|
-
}
|
|
276
|
-
list.push(relatingDef);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
573
|
+
// Use a toUpperCase cache across relationship refs (same type name set)
|
|
574
|
+
const typeUpperCache = new Map();
|
|
575
|
+
const getTypeUpper = (type) => {
|
|
576
|
+
let u = typeUpperCache.get(type);
|
|
577
|
+
if (u === undefined) {
|
|
578
|
+
u = type.toUpperCase();
|
|
579
|
+
typeUpperCache.set(type, u);
|
|
282
580
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// [0] GlobalId, [1] OwnerHistory, [2] Name, [3] Description
|
|
294
|
-
// [4] RelatedObjects (list of element IDs)
|
|
295
|
-
// [5] RelatingClassification / RelatingMaterial / RelatingDocument
|
|
296
|
-
const relatedObjects = attrs[4];
|
|
297
|
-
const relatingRef = attrs[5];
|
|
298
|
-
if (typeof relatingRef === 'number' && Array.isArray(relatedObjects)) {
|
|
299
|
-
const typeUpper = ref.type.toUpperCase();
|
|
300
|
-
if (typeUpper === 'IFCRELASSOCIATESCLASSIFICATION') {
|
|
301
|
-
for (const objId of relatedObjects) {
|
|
302
|
-
if (typeof objId === 'number') {
|
|
303
|
-
let list = onDemandClassificationMap.get(objId);
|
|
304
|
-
if (!list) {
|
|
305
|
-
list = [];
|
|
306
|
-
onDemandClassificationMap.set(objId, list);
|
|
307
|
-
}
|
|
308
|
-
list.push(relatingRef);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else if (typeUpper === 'IFCRELASSOCIATESMATERIAL') {
|
|
313
|
-
// IFC allows multiple IfcRelAssociatesMaterial per element but typically
|
|
314
|
-
// only one is valid. Last-write-wins: later relationships override earlier ones.
|
|
315
|
-
for (const objId of relatedObjects) {
|
|
316
|
-
if (typeof objId === 'number') {
|
|
317
|
-
onDemandMaterialMap.set(objId, relatingRef);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
else if (typeUpper === 'IFCRELASSOCIATESDOCUMENT') {
|
|
322
|
-
for (const objId of relatedObjects) {
|
|
323
|
-
if (typeof objId === 'number') {
|
|
324
|
-
let list = onDemandDocumentMap.get(objId);
|
|
325
|
-
if (!list) {
|
|
326
|
-
list = [];
|
|
327
|
-
onDemandDocumentMap.set(objId, list);
|
|
328
|
-
}
|
|
329
|
-
list.push(relatingRef);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
581
|
+
return u;
|
|
582
|
+
};
|
|
583
|
+
for (const ref of relationshipRefs) {
|
|
584
|
+
const typeUpper = getTypeUpper(ref.type);
|
|
585
|
+
const rel = extractRelFast(uint8Buffer, ref.byteOffset, ref.byteLength, typeUpper);
|
|
586
|
+
if (rel) {
|
|
587
|
+
const relType = REL_TYPE_MAP[typeUpper];
|
|
588
|
+
if (relType) {
|
|
589
|
+
for (const targetId of rel.relatedObjects) {
|
|
590
|
+
relationshipGraphBuilder.addEdge(rel.relatingObject, targetId, relType, ref.expressId);
|
|
332
591
|
}
|
|
333
592
|
}
|
|
334
593
|
}
|
|
335
594
|
}
|
|
336
|
-
|
|
595
|
+
logPhase('byte-level relationships');
|
|
596
|
+
// === BUILD ENTITY TABLE from categorized arrays ===
|
|
597
|
+
// Instead of iterating ALL 4.4M entityRefs, iterate only categorized arrays
|
|
598
|
+
// (~100K-200K total). This eliminates a 200-300ms loop over 4.4M items.
|
|
337
599
|
options.onProgress?.({ phase: 'building entities', percent: 30 });
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL', 'IFCFLOWCONTROLLER', 'IFCFLOWFITTING',
|
|
346
|
-
'IFCSPACE', 'IFCOPENINGELEMENT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
|
|
347
|
-
'IFCPROJECT', 'IFCCOVERING', 'IFCANNOTATION', 'IFCGRID',
|
|
348
|
-
]);
|
|
349
|
-
let processed = 0;
|
|
350
|
-
let added = 0;
|
|
351
|
-
for (const ref of entityRefs) {
|
|
352
|
-
const typeUpper = ref.type.toUpperCase();
|
|
353
|
-
// Skip non-relevant entities (geometric primitives, etc.)
|
|
354
|
-
const hasGeometry = GEOMETRY_TYPES.has(typeUpper);
|
|
355
|
-
const isType = isIfcTypeLikeEntity(typeUpper);
|
|
356
|
-
const isSpatial = SPATIAL_TYPES.has(typeUpper);
|
|
357
|
-
const isRelevant = hasGeometry || isType || isSpatial ||
|
|
358
|
-
RELEVANT_ENTITY_PREFIXES.has(typeUpper) ||
|
|
359
|
-
typeUpper.startsWith('IFCREL') || // Keep relationships for hierarchy
|
|
360
|
-
onDemandPropertyMap.has(ref.expressId) || // Keep entities with properties
|
|
361
|
-
onDemandQuantityMap.has(ref.expressId); // Keep entities with quantities
|
|
362
|
-
if (!isRelevant) {
|
|
363
|
-
processed++;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
// Get parsed data (GlobalId, Name) for spatial and geometry entities
|
|
367
|
-
const entityData = parsedEntityData.get(ref.expressId);
|
|
368
|
-
const globalId = entityData?.globalId || '';
|
|
369
|
-
const name = entityData?.name || '';
|
|
370
|
-
entityTableBuilder.add(ref.expressId, ref.type, globalId, name, '', // description
|
|
371
|
-
'', // objectType
|
|
372
|
-
hasGeometry, isType);
|
|
373
|
-
added++;
|
|
374
|
-
processed++;
|
|
375
|
-
// Yield every 10000 entities for better interleaving with geometry streaming
|
|
376
|
-
if (processed % 10000 === 0) {
|
|
377
|
-
options.onProgress?.({ phase: 'building entities', percent: 30 + (processed / totalEntities) * 50 });
|
|
378
|
-
// Direct yield - don't use maybeYield since we're already throttling
|
|
379
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
600
|
+
// Helper to add entities with pre-parsed data
|
|
601
|
+
const addEntityBatch = (refs, hasGeometry, isType) => {
|
|
602
|
+
for (const ref of refs) {
|
|
603
|
+
const entityData = parsedEntityData.get(ref.expressId);
|
|
604
|
+
entityTableBuilder.add(ref.expressId, ref.type, entityData?.globalId || '', entityData?.name || '', '', // description
|
|
605
|
+
'', // objectType
|
|
606
|
+
hasGeometry, isType);
|
|
380
607
|
}
|
|
381
|
-
}
|
|
608
|
+
};
|
|
609
|
+
addEntityBatch(spatialRefs, false, false);
|
|
610
|
+
addEntityBatch(geometryRefs, true, false);
|
|
611
|
+
addEntityBatch(typeObjectRefs, false, true);
|
|
612
|
+
addEntityBatch(relationshipRefs, false, false);
|
|
613
|
+
addEntityBatch(otherRelevantRefs, false, false);
|
|
614
|
+
logPhase('add entity batches');
|
|
382
615
|
const entityTable = entityTableBuilder.build();
|
|
616
|
+
logPhase('entity table build()');
|
|
383
617
|
// Empty property/quantity tables - use on-demand extraction instead
|
|
384
618
|
const propertyTable = propertyTableBuilder.build();
|
|
385
619
|
const quantityTable = quantityTableBuilder.build();
|
|
386
|
-
|
|
620
|
+
// Build intermediate relationship graph (spatial/hierarchy edges only).
|
|
621
|
+
// Property/association edges are added later; final graph is rebuilt at the end.
|
|
622
|
+
const hierarchyRelGraph = relationshipGraphBuilder.build();
|
|
623
|
+
logPhase('hierarchy rel graph build()');
|
|
624
|
+
await yieldIfNeeded();
|
|
387
625
|
// === EXTRACT LENGTH UNIT SCALE ===
|
|
388
626
|
options.onProgress?.({ phase: 'extracting units', percent: 85 });
|
|
389
627
|
const lengthUnitScale = extractLengthUnitScale(uint8Buffer, entityIndex);
|
|
@@ -392,57 +630,134 @@ export class ColumnarParser {
|
|
|
392
630
|
let spatialHierarchy;
|
|
393
631
|
try {
|
|
394
632
|
const hierarchyBuilder = new SpatialHierarchyBuilder();
|
|
395
|
-
spatialHierarchy = hierarchyBuilder.build(entityTable,
|
|
633
|
+
spatialHierarchy = hierarchyBuilder.build(entityTable, hierarchyRelGraph, strings, uint8Buffer, entityIndex, lengthUnitScale);
|
|
634
|
+
logPhase('spatial hierarchy');
|
|
396
635
|
}
|
|
397
636
|
catch (error) {
|
|
398
637
|
console.warn('[ColumnarParser] Failed to build spatial hierarchy:', error);
|
|
399
638
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
639
|
+
// === EMIT SPATIAL HIERARCHY EARLY ===
|
|
640
|
+
// The hierarchy panel can render immediately while property/association
|
|
641
|
+
// parsing continues. This lets the panel appear at the same time as
|
|
642
|
+
// geometry streaming completes.
|
|
643
|
+
const earlyStore = {
|
|
403
644
|
fileSize: buffer.byteLength,
|
|
404
645
|
schemaVersion,
|
|
405
646
|
entityCount: totalEntities,
|
|
406
|
-
parseTime,
|
|
647
|
+
parseTime: performance.now() - startTime,
|
|
407
648
|
source: uint8Buffer,
|
|
408
649
|
entityIndex,
|
|
409
650
|
strings,
|
|
410
651
|
entities: entityTable,
|
|
411
652
|
properties: propertyTable,
|
|
412
653
|
quantities: quantityTable,
|
|
413
|
-
relationships:
|
|
654
|
+
relationships: hierarchyRelGraph,
|
|
414
655
|
spatialHierarchy,
|
|
415
|
-
onDemandPropertyMap, // For instant property access
|
|
416
|
-
onDemandQuantityMap, // For instant quantity access
|
|
417
|
-
onDemandClassificationMap, // For instant classification access
|
|
418
|
-
onDemandMaterialMap, // For instant material access
|
|
419
|
-
onDemandDocumentMap, // For instant document access
|
|
420
656
|
};
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
657
|
+
options.onSpatialReady?.(earlyStore);
|
|
658
|
+
await yieldIfNeeded(); // Let geometry process after hierarchy emission
|
|
659
|
+
// === DEFERRED: Parse property and association relationships ===
|
|
660
|
+
// These are NOT needed for the spatial hierarchy panel.
|
|
661
|
+
options.onProgress?.({ phase: 'parsing property refs', percent: 92 });
|
|
662
|
+
const onDemandPropertyMap = new Map();
|
|
663
|
+
const onDemandQuantityMap = new Map();
|
|
664
|
+
// Pre-build Sets of property set / quantity set IDs from already-categorized refs.
|
|
665
|
+
// This replaces 252K binary searches on the 14M compact entity index with O(1) Set lookups.
|
|
666
|
+
const propertySetIds = new Set();
|
|
667
|
+
const quantitySetIds = new Set();
|
|
668
|
+
for (const ref of propertyEntityRefs) {
|
|
669
|
+
const tu = getTypeUpper(ref.type);
|
|
670
|
+
if (tu === 'IFCPROPERTYSET')
|
|
671
|
+
propertySetIds.add(ref.expressId);
|
|
672
|
+
else if (tu === 'IFCELEMENTQUANTITY')
|
|
673
|
+
quantitySetIds.add(ref.expressId);
|
|
434
674
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
675
|
+
// Property rels: byte-level scanning + addEdge (now fast with SoA builder).
|
|
676
|
+
let totalPropRelObjects = 0;
|
|
677
|
+
for (let pi = 0; pi < propertyRelRefs.length; pi++) {
|
|
678
|
+
if ((pi & 0x3FF) === 0)
|
|
679
|
+
await yieldIfNeeded();
|
|
680
|
+
const ref = propertyRelRefs[pi];
|
|
681
|
+
const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
|
|
682
|
+
if (result) {
|
|
683
|
+
const { relatedObjects, relatingDef } = result;
|
|
684
|
+
totalPropRelObjects += relatedObjects.length;
|
|
685
|
+
for (const objId of relatedObjects) {
|
|
686
|
+
relationshipGraphBuilder.addEdge(relatingDef, objId, RelationshipType.DefinesByProperties, ref.expressId);
|
|
687
|
+
}
|
|
688
|
+
// Build on-demand property/quantity maps using pre-built Sets (O(1) vs binary search)
|
|
689
|
+
const isPropSet = propertySetIds.has(relatingDef);
|
|
690
|
+
const isQtySet = !isPropSet && quantitySetIds.has(relatingDef);
|
|
691
|
+
if (isPropSet || isQtySet) {
|
|
692
|
+
const targetMap = isPropSet ? onDemandPropertyMap : onDemandQuantityMap;
|
|
693
|
+
for (const objId of relatedObjects) {
|
|
694
|
+
let list = targetMap.get(objId);
|
|
695
|
+
if (!list) {
|
|
696
|
+
list = [];
|
|
697
|
+
targetMap.set(objId, list);
|
|
698
|
+
}
|
|
699
|
+
list.push(relatingDef);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
438
703
|
}
|
|
439
|
-
|
|
440
|
-
|
|
704
|
+
console.log(`[parseLite] propertyRels: ${propertyRelRefs.length} rels, ${totalPropRelObjects} total relatedObjects`);
|
|
705
|
+
await yieldIfNeeded();
|
|
706
|
+
// Association rels: byte-level scanning, no addEdge (same reasoning as property rels)
|
|
707
|
+
options.onProgress?.({ phase: 'parsing associations', percent: 95 });
|
|
708
|
+
const onDemandClassificationMap = new Map();
|
|
709
|
+
const onDemandMaterialMap = new Map();
|
|
710
|
+
const onDemandDocumentMap = new Map();
|
|
711
|
+
for (const ref of associationRelRefs) {
|
|
712
|
+
const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
|
|
713
|
+
if (result) {
|
|
714
|
+
const { relatedObjects, relatingDef: relatingRef } = result;
|
|
715
|
+
const typeUpper = getTypeUpper(ref.type);
|
|
716
|
+
if (typeUpper === 'IFCRELASSOCIATESCLASSIFICATION') {
|
|
717
|
+
for (const objId of relatedObjects) {
|
|
718
|
+
let list = onDemandClassificationMap.get(objId);
|
|
719
|
+
if (!list) {
|
|
720
|
+
list = [];
|
|
721
|
+
onDemandClassificationMap.set(objId, list);
|
|
722
|
+
}
|
|
723
|
+
list.push(relatingRef);
|
|
724
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesClassification, ref.expressId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else if (typeUpper === 'IFCRELASSOCIATESMATERIAL') {
|
|
728
|
+
for (const objId of relatedObjects) {
|
|
729
|
+
onDemandMaterialMap.set(objId, relatingRef);
|
|
730
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesMaterial, ref.expressId);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else if (typeUpper === 'IFCRELASSOCIATESDOCUMENT') {
|
|
734
|
+
for (const objId of relatedObjects) {
|
|
735
|
+
let list = onDemandDocumentMap.get(objId);
|
|
736
|
+
if (!list) {
|
|
737
|
+
list = [];
|
|
738
|
+
onDemandDocumentMap.set(objId, list);
|
|
739
|
+
}
|
|
740
|
+
list.push(relatingRef);
|
|
741
|
+
relationshipGraphBuilder.addEdge(relatingRef, objId, RelationshipType.AssociatesDocument, ref.expressId);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
441
745
|
}
|
|
746
|
+
logPhase('property+association rels');
|
|
747
|
+
// Rebuild relationship graph with ALL edges (hierarchy + property + association)
|
|
748
|
+
const fullRelationshipGraph = relationshipGraphBuilder.build();
|
|
749
|
+
logPhase('relationship graph build()');
|
|
750
|
+
const parseTime = performance.now() - startTime;
|
|
751
|
+
options.onProgress?.({ phase: 'complete', percent: 100 });
|
|
442
752
|
return {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
753
|
+
...earlyStore,
|
|
754
|
+
parseTime,
|
|
755
|
+
relationships: fullRelationshipGraph,
|
|
756
|
+
onDemandPropertyMap,
|
|
757
|
+
onDemandQuantityMap,
|
|
758
|
+
onDemandClassificationMap,
|
|
759
|
+
onDemandMaterialMap,
|
|
760
|
+
onDemandDocumentMap,
|
|
446
761
|
};
|
|
447
762
|
}
|
|
448
763
|
/**
|