@ifc-lite/parser 2.1.7 → 2.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/classification-extractor.d.ts +1 -1
  2. package/dist/classification-extractor.d.ts.map +1 -1
  3. package/dist/classification-resolver.d.ts +18 -0
  4. package/dist/classification-resolver.d.ts.map +1 -0
  5. package/dist/classification-resolver.js +126 -0
  6. package/dist/classification-resolver.js.map +1 -0
  7. package/dist/columnar-parser-attributes.d.ts +39 -0
  8. package/dist/columnar-parser-attributes.d.ts.map +1 -0
  9. package/dist/columnar-parser-attributes.js +225 -0
  10. package/dist/columnar-parser-attributes.js.map +1 -0
  11. package/dist/columnar-parser-indexes.d.ts +42 -0
  12. package/dist/columnar-parser-indexes.d.ts.map +1 -0
  13. package/dist/columnar-parser-indexes.js +102 -0
  14. package/dist/columnar-parser-indexes.js.map +1 -0
  15. package/dist/columnar-parser-relationships.d.ts +17 -0
  16. package/dist/columnar-parser-relationships.d.ts.map +1 -0
  17. package/dist/columnar-parser-relationships.js +95 -0
  18. package/dist/columnar-parser-relationships.js.map +1 -0
  19. package/dist/columnar-parser.d.ts +6 -21
  20. package/dist/columnar-parser.d.ts.map +1 -1
  21. package/dist/columnar-parser.js +154 -445
  22. package/dist/columnar-parser.js.map +1 -1
  23. package/dist/compact-entity-index.d.ts +1 -0
  24. package/dist/compact-entity-index.d.ts.map +1 -1
  25. package/dist/compact-entity-index.js +52 -0
  26. package/dist/compact-entity-index.js.map +1 -1
  27. package/dist/georef-extractor.d.ts +7 -1
  28. package/dist/georef-extractor.d.ts.map +1 -1
  29. package/dist/georef-extractor.js +29 -5
  30. package/dist/georef-extractor.js.map +1 -1
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +24 -17
  34. package/dist/index.js.map +1 -1
  35. package/dist/material-extractor.d.ts +1 -1
  36. package/dist/material-extractor.d.ts.map +1 -1
  37. package/dist/material-resolver.d.ts +37 -0
  38. package/dist/material-resolver.d.ts.map +1 -0
  39. package/dist/material-resolver.js +230 -0
  40. package/dist/material-resolver.js.map +1 -0
  41. package/dist/on-demand-extractors.d.ts +4 -51
  42. package/dist/on-demand-extractors.d.ts.map +1 -1
  43. package/dist/on-demand-extractors.js +17 -341
  44. package/dist/on-demand-extractors.js.map +1 -1
  45. package/dist/style-extractor.js.map +1 -1
  46. package/dist/worker-parser.d.ts.map +1 -1
  47. package/dist/worker-parser.js +2 -4
  48. package/dist/worker-parser.js.map +1 -1
  49. package/package.json +1 -1
@@ -4,402 +4,13 @@
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 { decodeIfcString } from '@ifc-lite/encoding';
8
7
  import { getAttributeNames, getInheritanceChain } from './ifc-schema.js';
9
8
  import { parsePropertyValue } from './on-demand-extractors.js';
10
- import { buildCompactEntityIndex } from './compact-entity-index.js';
9
+ import { buildCompactEntityIndexAsync } from './compact-entity-index.js';
11
10
  import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, } from '@ifc-lite/data';
12
- // Pre-computed type sets for O(1) lookups
13
- const GEOMETRY_TYPES = new Set([
14
- 'IFCWALL', 'IFCWALLSTANDARDCASE', 'IFCDOOR', 'IFCWINDOW', 'IFCSLAB',
15
- 'IFCCOLUMN', 'IFCBEAM', 'IFCROOF', 'IFCSTAIR', 'IFCSTAIRFLIGHT',
16
- 'IFCRAILING', 'IFCRAMP', 'IFCRAMPFLIGHT', 'IFCPLATE', 'IFCMEMBER',
17
- 'IFCCURTAINWALL', 'IFCFOOTING', 'IFCPILE', 'IFCBUILDINGELEMENTPROXY',
18
- 'IFCFURNISHINGELEMENT', 'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL',
19
- 'IFCFLOWCONTROLLER', 'IFCFLOWFITTING', 'IFCSPACE', 'IFCOPENINGELEMENT',
20
- 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
21
- ]);
22
- // IMPORTANT: This set MUST include ALL RelationshipType enum values to prevent semantic loss
23
- // Missing types will be skipped during parsing, causing incomplete relationship graphs
24
- const RELATIONSHIP_TYPES = new Set([
25
- 'IFCRELCONTAINEDINSPATIALSTRUCTURE', 'IFCRELAGGREGATES',
26
- 'IFCRELDEFINESBYPROPERTIES', 'IFCRELDEFINESBYTYPE',
27
- 'IFCRELASSOCIATESMATERIAL', 'IFCRELASSOCIATESCLASSIFICATION',
28
- 'IFCRELASSOCIATESDOCUMENT',
29
- 'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
30
- 'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
31
- 'IFCRELSPACEBOUNDARY',
32
- 'IFCRELASSIGNSTOGROUP', 'IFCRELASSIGNSTOPRODUCT',
33
- 'IFCRELREFERENCEDINSPATIALSTRUCTURE',
34
- ]);
35
- // Map IFC relationship type strings to RelationshipType enum
36
- // MUST cover ALL RelationshipType enum values (14 types total)
37
- const REL_TYPE_MAP = {
38
- 'IFCRELCONTAINEDINSPATIALSTRUCTURE': RelationshipType.ContainsElements,
39
- 'IFCRELAGGREGATES': RelationshipType.Aggregates,
40
- 'IFCRELDEFINESBYPROPERTIES': RelationshipType.DefinesByProperties,
41
- 'IFCRELDEFINESBYTYPE': RelationshipType.DefinesByType,
42
- 'IFCRELASSOCIATESMATERIAL': RelationshipType.AssociatesMaterial,
43
- 'IFCRELASSOCIATESCLASSIFICATION': RelationshipType.AssociatesClassification,
44
- 'IFCRELASSOCIATESDOCUMENT': RelationshipType.AssociatesDocument,
45
- 'IFCRELVOIDSELEMENT': RelationshipType.VoidsElement,
46
- 'IFCRELFILLSELEMENT': RelationshipType.FillsElement,
47
- 'IFCRELCONNECTSPATHELEMENTS': RelationshipType.ConnectsPathElements,
48
- 'IFCRELCONNECTSELEMENTS': RelationshipType.ConnectsElements,
49
- 'IFCRELSPACEBOUNDARY': RelationshipType.SpaceBoundary,
50
- 'IFCRELASSIGNSTOGROUP': RelationshipType.AssignsToGroup,
51
- 'IFCRELASSIGNSTOPRODUCT': RelationshipType.AssignsToProduct,
52
- 'IFCRELREFERENCEDINSPATIALSTRUCTURE': RelationshipType.ReferencedInSpatialStructure,
53
- };
54
- const QUANTITY_TYPE_MAP = {
55
- 'IFCQUANTITYLENGTH': QuantityType.Length,
56
- 'IFCQUANTITYAREA': QuantityType.Area,
57
- 'IFCQUANTITYVOLUME': QuantityType.Volume,
58
- 'IFCQUANTITYCOUNT': QuantityType.Count,
59
- 'IFCQUANTITYWEIGHT': QuantityType.Weight,
60
- 'IFCQUANTITYTIME': QuantityType.Time,
61
- };
62
- // Types needed for spatial hierarchy (small subset)
63
- const SPATIAL_TYPES = new Set([
64
- 'IFCPROJECT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY', 'IFCSPACE',
65
- 'IFCFACILITY', 'IFCFACILITYPART',
66
- 'IFCBRIDGE', 'IFCBRIDGEPART',
67
- 'IFCROAD', 'IFCROADPART',
68
- 'IFCRAILWAY', 'IFCRAILWAYPART',
69
- 'IFCMARINEFACILITY',
70
- ]);
71
- // Relationship types needed for hierarchy and structural relationships
72
- const HIERARCHY_REL_TYPES = new Set([
73
- 'IFCRELAGGREGATES', 'IFCRELCONTAINEDINSPATIALSTRUCTURE',
74
- 'IFCRELDEFINESBYTYPE',
75
- // Structural relationships (voids, fills, connections, groups)
76
- 'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
77
- 'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
78
- 'IFCRELSPACEBOUNDARY',
79
- 'IFCRELASSIGNSTOGROUP', 'IFCRELASSIGNSTOPRODUCT',
80
- 'IFCRELREFERENCEDINSPATIALSTRUCTURE',
81
- ]);
82
- // Relationship types for on-demand property loading
83
- const PROPERTY_REL_TYPES = new Set([
84
- 'IFCRELDEFINESBYPROPERTIES',
85
- ]);
86
- // Relationship types for on-demand classification/material loading
87
- const ASSOCIATION_REL_TYPES = new Set([
88
- 'IFCRELASSOCIATESCLASSIFICATION', 'IFCRELASSOCIATESMATERIAL',
89
- 'IFCRELASSOCIATESDOCUMENT',
90
- ]);
91
- // Attributes to skip in extractAllEntityAttributes (shown elsewhere or non-displayable)
92
- const SKIP_DISPLAY_ATTRS = new Set(['GlobalId', 'OwnerHistory', 'ObjectPlacement', 'Representation', 'HasPropertySets', 'RepresentationMaps']);
93
- // Property-related entity types for on-demand extraction
94
- const PROPERTY_ENTITY_TYPES = new Set([
95
- 'IFCPROPERTYSET', 'IFCELEMENTQUANTITY',
96
- 'IFCPROPERTYSINGLEVALUE', 'IFCPROPERTYENUMERATEDVALUE',
97
- 'IFCPROPERTYBOUNDEDVALUE', 'IFCPROPERTYTABLEVALUE',
98
- 'IFCPROPERTYLISTVALUE', 'IFCPROPERTYREFERENCEVALUE',
99
- 'IFCQUANTITYLENGTH', 'IFCQUANTITYAREA', 'IFCQUANTITYVOLUME',
100
- 'IFCQUANTITYCOUNT', 'IFCQUANTITYWEIGHT', 'IFCQUANTITYTIME',
101
- ]);
102
- function isIfcTypeLikeEntity(typeUpper) {
103
- return typeUpper.endsWith('TYPE') || typeUpper.endsWith('STYLE');
104
- }
105
- // ==========================================
106
- // Byte-level helpers for fast extraction
107
- // These avoid per-entity TextDecoder calls by working on raw bytes.
108
- // ==========================================
109
- /**
110
- * Find the byte range of a quoted string at a specific attribute position in STEP entity bytes.
111
- * Returns [start, end) byte offsets (excluding quotes), or null if not found.
112
- *
113
- * @param buffer - The IFC file buffer
114
- * @param entityStart - byte offset of the entity
115
- * @param entityLen - byte length of the entity
116
- * @param attrIndex - 0-based attribute index (0=GlobalId, 2=Name)
117
- */
118
- function findQuotedAttrRange(buffer, entityStart, entityLen, attrIndex) {
119
- const end = entityStart + entityLen;
120
- let pos = entityStart;
121
- // Skip to opening paren '(' after TYPE name
122
- while (pos < end && buffer[pos] !== 0x28 /* ( */)
123
- pos++;
124
- if (pos >= end)
125
- return null;
126
- pos++; // skip '('
127
- // Skip commas to reach the target attribute
128
- if (attrIndex > 0) {
129
- let toSkip = attrIndex;
130
- let depth = 0;
131
- let inStr = false;
132
- while (pos < end && toSkip > 0) {
133
- const ch = buffer[pos];
134
- if (ch === 0x27 /* ' */) {
135
- if (inStr && pos + 1 < end && buffer[pos + 1] === 0x27) {
136
- pos += 2;
137
- continue;
138
- }
139
- inStr = !inStr;
140
- }
141
- else if (!inStr) {
142
- if (ch === 0x28)
143
- depth++;
144
- else if (ch === 0x29)
145
- depth--;
146
- else if (ch === 0x2C && depth === 0)
147
- toSkip--;
148
- }
149
- pos++;
150
- }
151
- }
152
- // Skip whitespace
153
- while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
154
- pos++;
155
- // Check for quoted string
156
- if (pos >= end || buffer[pos] !== 0x27 /* ' */)
157
- return null;
158
- pos++; // skip opening quote
159
- const start = pos;
160
- // Find closing quote (handle escaped quotes '')
161
- while (pos < end) {
162
- if (buffer[pos] === 0x27) {
163
- if (pos + 1 < end && buffer[pos + 1] === 0x27) {
164
- pos += 2;
165
- continue;
166
- }
167
- break;
168
- }
169
- pos++;
170
- }
171
- return [start, pos];
172
- }
173
- /**
174
- * Batch extract GlobalId (attr[0]) and Name (attr[2]) for many entities using
175
- * only 2 TextDecoder.decode() calls total (one for all GlobalIds, one for all Names).
176
- *
177
- * This is ~100x faster than calling extractEntity() per entity for large batches
178
- * because it eliminates per-entity TextDecoder overhead which is significant in Firefox.
179
- *
180
- * Returns a Map from expressId → { globalId, name }.
181
- */
182
- function batchExtractGlobalIdAndName(buffer, refs) {
183
- const result = new Map();
184
- if (refs.length === 0)
185
- return result;
186
- // Phase 1: Scan byte ranges for GlobalId and Name positions (no string allocation)
187
- const gidRanges = []; // [start, end) for each entity
188
- const nameRanges = [];
189
- const validIndices = []; // indices into refs for entities with valid ranges
190
- for (let i = 0; i < refs.length; i++) {
191
- const ref = refs[i];
192
- const gidRange = findQuotedAttrRange(buffer, ref.byteOffset, ref.byteLength, 0);
193
- const nameRange = findQuotedAttrRange(buffer, ref.byteOffset, ref.byteLength, 2);
194
- gidRanges.push(gidRange ?? [0, 0]);
195
- nameRanges.push(nameRange ?? [0, 0]);
196
- validIndices.push(i);
197
- }
198
- // Phase 2: Concatenate all GlobalId bytes into one buffer, decode once
199
- // Use null byte (0x00) as separator (never appears in IFC string content)
200
- let totalGidBytes = 0;
201
- let totalNameBytes = 0;
202
- for (let i = 0; i < validIndices.length; i++) {
203
- const [gs, ge] = gidRanges[i];
204
- const [ns, ne] = nameRanges[i];
205
- totalGidBytes += (ge - gs) + 1; // +1 for separator
206
- totalNameBytes += (ne - ns) + 1;
207
- }
208
- const gidBuf = new Uint8Array(totalGidBytes);
209
- const nameBuf = new Uint8Array(totalNameBytes);
210
- let gidOffset = 0;
211
- let nameOffset = 0;
212
- for (let i = 0; i < validIndices.length; i++) {
213
- const [gs, ge] = gidRanges[i];
214
- const [ns, ne] = nameRanges[i];
215
- if (ge > gs) {
216
- gidBuf.set(buffer.subarray(gs, ge), gidOffset);
217
- gidOffset += ge - gs;
218
- }
219
- gidBuf[gidOffset++] = 0; // null separator
220
- if (ne > ns) {
221
- nameBuf.set(buffer.subarray(ns, ne), nameOffset);
222
- nameOffset += ne - ns;
223
- }
224
- nameBuf[nameOffset++] = 0;
225
- }
226
- // Phase 3: Two TextDecoder calls for ALL entities
227
- const decoder = new TextDecoder();
228
- const allGids = decoder.decode(gidBuf.subarray(0, gidOffset));
229
- const allNames = decoder.decode(nameBuf.subarray(0, nameOffset));
230
- const gids = allGids.split('\0');
231
- const names = allNames.split('\0');
232
- // Phase 4: Build result map
233
- for (let i = 0; i < validIndices.length; i++) {
234
- const ref = refs[validIndices[i]];
235
- const rawName = names[i] || '';
236
- result.set(ref.expressId, {
237
- globalId: gids[i] || '',
238
- name: rawName ? decodeIfcString(rawName) : '',
239
- });
240
- }
241
- return result;
242
- }
243
- // ==========================================
244
- // Byte-level relationship scanners (numbers only, no TextDecoder)
245
- // ==========================================
246
- /**
247
- * Skip N commas at depth 0 in STEP bytes.
248
- */
249
- function skipCommas(buffer, start, end, count) {
250
- let pos = start;
251
- let remaining = count;
252
- let depth = 0;
253
- let inString = false;
254
- while (pos < end && remaining > 0) {
255
- const ch = buffer[pos];
256
- if (ch === 0x27) {
257
- if (inString && pos + 1 < end && buffer[pos + 1] === 0x27) {
258
- pos += 2;
259
- continue;
260
- }
261
- inString = !inString;
262
- }
263
- else if (!inString) {
264
- if (ch === 0x28)
265
- depth++;
266
- else if (ch === 0x29)
267
- depth--;
268
- else if (ch === 0x2C && depth === 0)
269
- remaining--;
270
- }
271
- pos++;
272
- }
273
- return pos;
274
- }
275
- /** Read a #ID entity reference as a number. Returns -1 if not an entity ref. */
276
- function readRefId(buffer, pos, end) {
277
- while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
278
- pos++;
279
- if (pos < end && buffer[pos] === 0x23) {
280
- pos++;
281
- let num = 0;
282
- while (pos < end && buffer[pos] >= 0x30 && buffer[pos] <= 0x39) {
283
- num = num * 10 + (buffer[pos] - 0x30);
284
- pos++;
285
- }
286
- return [num, pos];
287
- }
288
- return [-1, pos];
289
- }
290
- /** Read a list of entity refs (#id1,#id2,...) or a single #id. Returns [ids, newPos]. */
291
- function readRefList(buffer, pos, end) {
292
- while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09))
293
- pos++;
294
- const ids = [];
295
- if (pos < end && buffer[pos] === 0x28) {
296
- pos++;
297
- while (pos < end && buffer[pos] !== 0x29) {
298
- while (pos < end && (buffer[pos] === 0x20 || buffer[pos] === 0x09 || buffer[pos] === 0x2C))
299
- pos++;
300
- if (pos < end && buffer[pos] === 0x23) {
301
- const [id, np] = readRefId(buffer, pos, end);
302
- if (id >= 0)
303
- ids.push(id);
304
- pos = np;
305
- }
306
- else if (pos < end && buffer[pos] !== 0x29) {
307
- pos++;
308
- }
309
- }
310
- }
311
- else if (pos < end && buffer[pos] === 0x23) {
312
- const [id, np] = readRefId(buffer, pos, end);
313
- if (id >= 0)
314
- ids.push(id);
315
- pos = np;
316
- }
317
- return [ids, pos];
318
- }
319
- /**
320
- * Extract relatingObject and relatedObjects from a relationship entity using byte-level scanning.
321
- * No TextDecoder needed - only extracts numeric entity IDs.
322
- */
323
- function extractRelFast(buffer, byteOffset, byteLength, typeUpper) {
324
- const end = byteOffset + byteLength;
325
- let pos = byteOffset;
326
- while (pos < end && buffer[pos] !== 0x28)
327
- pos++;
328
- if (pos >= end)
329
- return null;
330
- pos++;
331
- // Skip to attr[4] (all IfcRelationship subtypes have 4 shared IfcRoot+IfcRelationship attrs)
332
- pos = skipCommas(buffer, pos, end, 4);
333
- if (typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE'
334
- || typeUpper === 'IFCRELREFERENCEDINSPATIALSTRUCTURE'
335
- || typeUpper === 'IFCRELDEFINESBYPROPERTIES'
336
- || typeUpper === 'IFCRELDEFINESBYTYPE') {
337
- // attr[4]=RelatedObjects, attr[5]=RelatingObject
338
- const [related, rp] = readRefList(buffer, pos, end);
339
- pos = rp;
340
- while (pos < end && buffer[pos] !== 0x2C)
341
- pos++;
342
- pos++;
343
- const [relating, _] = readRefId(buffer, pos, end);
344
- if (relating < 0 || related.length === 0)
345
- return null;
346
- return { relatingObject: relating, relatedObjects: related };
347
- }
348
- else if (typeUpper === 'IFCRELASSIGNSTOGROUP' || typeUpper === 'IFCRELASSIGNSTOPRODUCT') {
349
- const [related, rp] = readRefList(buffer, pos, end);
350
- pos = skipCommas(buffer, rp, end, 2);
351
- const [relating, _] = readRefId(buffer, pos, end);
352
- if (relating < 0 || related.length === 0)
353
- return null;
354
- return { relatingObject: relating, relatedObjects: related };
355
- }
356
- else if (typeUpper === 'IFCRELCONNECTSELEMENTS' || typeUpper === 'IFCRELCONNECTSPATHELEMENTS') {
357
- pos = skipCommas(buffer, pos, end, 1);
358
- const [relating, rp2] = readRefId(buffer, pos, end);
359
- pos = skipCommas(buffer, rp2, end, 1);
360
- const [related, _] = readRefId(buffer, pos, end);
361
- if (relating < 0 || related < 0)
362
- return null;
363
- return { relatingObject: relating, relatedObjects: [related] };
364
- }
365
- else {
366
- // Default: attr[4]=RelatingObject, attr[5]=RelatedObject(s)
367
- const [relating, rp] = readRefId(buffer, pos, end);
368
- if (relating < 0)
369
- return null;
370
- pos = rp;
371
- while (pos < end && buffer[pos] !== 0x2C)
372
- pos++;
373
- pos++;
374
- const [related, _] = readRefList(buffer, pos, end);
375
- if (related.length === 0)
376
- return null;
377
- return { relatingObject: relating, relatedObjects: related };
378
- }
379
- }
380
- /**
381
- * Extract property rel data: attr[4]=relatedObjects, attr[5]=relatingDef.
382
- * Numbers only, no TextDecoder.
383
- */
384
- function extractPropertyRelFast(buffer, byteOffset, byteLength) {
385
- const end = byteOffset + byteLength;
386
- let pos = byteOffset;
387
- while (pos < end && buffer[pos] !== 0x28)
388
- pos++;
389
- if (pos >= end)
390
- return null;
391
- pos++;
392
- pos = skipCommas(buffer, pos, end, 4);
393
- const [relatedObjects, rp] = readRefList(buffer, pos, end);
394
- pos = rp;
395
- while (pos < end && buffer[pos] !== 0x2C)
396
- pos++;
397
- pos++;
398
- const [relatingDef, _] = readRefId(buffer, pos, end);
399
- if (relatingDef < 0 || relatedObjects.length === 0)
400
- return null;
401
- return { relatedObjects, relatingDef };
402
- }
11
+ import { batchExtractGlobalIdAndName } from './columnar-parser-attributes.js';
12
+ import { GEOMETRY_TYPES, REL_TYPE_MAP, QUANTITY_TYPE_MAP, SPATIAL_TYPES, HIERARCHY_REL_TYPES, PROPERTY_REL_TYPES, ASSOCIATION_REL_TYPES, SKIP_DISPLAY_ATTRS, PROPERTY_ENTITY_TYPES, PROPERTY_CONTAINER_TYPES, isIfcTypeLikeEntity, } from './columnar-parser-indexes.js';
13
+ import { extractRelFast, extractPropertyRelFast } from './columnar-parser-relationships.js';
403
14
  function detectSchemaVersion(buffer) {
404
15
  const headerEnd = Math.min(buffer.length, 2000);
405
16
  const headerText = new TextDecoder().decode(buffer.subarray(0, headerEnd)).toUpperCase();
@@ -413,6 +24,17 @@ function detectSchemaVersion(buffer) {
413
24
  return 'IFC2X3';
414
25
  return 'IFC4'; // Default fallback
415
26
  }
27
+ function yieldToEventLoop() {
28
+ const maybeScheduler = globalThis.scheduler;
29
+ if (typeof maybeScheduler?.yield === 'function') {
30
+ return maybeScheduler.yield();
31
+ }
32
+ return new Promise((resolve) => {
33
+ const channel = new MessageChannel();
34
+ channel.port1.onmessage = () => resolve();
35
+ channel.port2.postMessage(null);
36
+ });
37
+ }
416
38
  export class ColumnarParser {
417
39
  /**
418
40
  * Parse IFC file into columnar data store
@@ -427,9 +49,14 @@ export class ColumnarParser {
427
49
  const totalEntities = entityRefs.length;
428
50
  // Phase timing for performance telemetry
429
51
  let phaseStart = startTime;
52
+ const emitDiagnostic = (message) => {
53
+ options.onDiagnostic?.(message);
54
+ };
430
55
  const logPhase = (name) => {
431
56
  const now = performance.now();
432
- console.log(`[parseLite] ${name}: ${Math.round(now - phaseStart)}ms`);
57
+ const elapsed = Math.round(now - phaseStart);
58
+ console.log(`[parseLite] ${name}: ${elapsed}ms`);
59
+ emitDiagnostic(`${name}: ${elapsed}ms`);
433
60
  phaseStart = now;
434
61
  };
435
62
  options.onProgress?.({ phase: 'building', percent: 0 });
@@ -441,13 +68,20 @@ export class ColumnarParser {
441
68
  const quantityTableBuilder = new QuantityTableBuilder(strings);
442
69
  const relationshipGraphBuilder = new RelationshipGraphBuilder();
443
70
  logPhase('init builders');
444
- // Build compact entity index (typed arrays instead of Map for ~3x memory reduction)
445
- const compactByIdIndex = buildCompactEntityIndex(entityRefs);
446
- logPhase('compact entity index');
447
71
  // Single pass: build byType index AND categorize entities simultaneously.
448
72
  // Uses a type-name cache to avoid calling .toUpperCase() on 4.4M refs
449
73
  // (only ~776 unique type names in IFC4).
450
74
  const byType = new Map();
75
+ const deferPropertyAtomIndex = options.deferPropertyAtomIndex === true;
76
+ const typeUpperCache = new Map();
77
+ const getTypeUpper = (type) => {
78
+ let upper = typeUpperCache.get(type);
79
+ if (upper === undefined) {
80
+ upper = type.toUpperCase();
81
+ typeUpperCache.set(type, upper);
82
+ }
83
+ return upper;
84
+ };
451
85
  const RELEVANT_ENTITY_PREFIXES = new Set([
452
86
  'IFCWALL', 'IFCSLAB', 'IFCBEAM', 'IFCCOLUMN', 'IFCPLATE', 'IFCDOOR', 'IFCWINDOW',
453
87
  'IFCROOF', 'IFCSTAIR', 'IFCRAILING', 'IFCRAMP', 'IFCFOOTING', 'IFCPILE',
@@ -455,6 +89,19 @@ export class ColumnarParser {
455
89
  'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL', 'IFCFLOWCONTROLLER', 'IFCFLOWFITTING',
456
90
  'IFCSPACE', 'IFCOPENINGELEMENT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
457
91
  'IFCPROJECT', 'IFCCOVERING', 'IFCANNOTATION', 'IFCGRID',
92
+ // Infrastructure entities needed by on-demand extraction and StepExporter.
93
+ // Without these, findPreferredGeometricRepresentationContextId() and
94
+ // findLengthUnitReference() fail because the entities are not in byId.
95
+ 'IFCGEOMETRICREPRESENTATIONCONTEXT', 'IFCGEOMETRICREPRESENTATIONSUBCONTEXT',
96
+ 'IFCUNITASSIGNMENT', 'IFCSIUNIT', 'IFCCONVERSIONBASEDUNIT',
97
+ 'IFCDERIVEDUNIT', 'IFCDERIVEDUNITELEMENT', 'IFCMEASUREWITHUNIT',
98
+ 'IFCDIMENSIONALEXPONENTS',
99
+ 'IFCMAPCONVERSION', 'IFCPROJECTEDCRS',
100
+ 'IFCMATERIALLAYER', 'IFCMATERIALLAYERSET', 'IFCMATERIALLAYERSETUSAGE',
101
+ 'IFCMATERIALCONSTITUENTSET', 'IFCMATERIALCONSTITUENT',
102
+ 'IFCMATERIALPROFILESET', 'IFCMATERIALPROFILE', 'IFCMATERIAL',
103
+ 'IFCCLASSIFICATION', 'IFCCLASSIFICATIONREFERENCE',
104
+ 'IFCDOCUMENTINFORMATION', 'IFCDOCUMENTREFERENCE',
458
105
  ]);
459
106
  // Category constants for the lookup cache
460
107
  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;
@@ -469,7 +116,7 @@ export class ColumnarParser {
469
116
  let cat = typeCategoryCache.get(type);
470
117
  if (cat !== undefined)
471
118
  return cat;
472
- const upper = type.toUpperCase();
119
+ const upper = getTypeUpper(type);
473
120
  if (SPATIAL_TYPES.has(upper) || isSubtypeOfAny(upper, SPATIAL_TYPES))
474
121
  cat = CAT_SPATIAL;
475
122
  else if (GEOMETRY_TYPES.has(upper) || isSubtypeOfAny(upper, GEOMETRY_TYPES))
@@ -491,24 +138,48 @@ export class ColumnarParser {
491
138
  typeCategoryCache.set(type, cat);
492
139
  return cat;
493
140
  }
141
+ // Time-based yielding: yield to the main thread every ~80ms so geometry
142
+ // streaming callbacks can fire. This limits main-thread blocking to short
143
+ // bursts that don't starve geometry, while adding minimal overhead (~15 yields
144
+ // × ~1ms each ≈ 15ms total over the full parse).
145
+ const YIELD_INTERVAL_MS = Math.max(16, options.yieldIntervalMs ?? 80);
146
+ let lastYieldTime = performance.now();
147
+ const yieldIfNeeded = async () => {
148
+ const now = performance.now();
149
+ if (now - lastYieldTime >= YIELD_INTERVAL_MS) {
150
+ await yieldToEventLoop();
151
+ lastYieldTime = performance.now();
152
+ }
153
+ };
154
+ emitDiagnostic(`parseLite start: totalEntities=${totalEntities} yieldInterval=${YIELD_INTERVAL_MS}ms`);
494
155
  const spatialRefs = [];
495
156
  const geometryRefs = [];
496
157
  const relationshipRefs = [];
497
158
  const propertyRelRefs = [];
498
- const propertyEntityRefs = [];
159
+ const propertyContainerRefs = [];
160
+ const propertyAtomRefs = [];
499
161
  const associationRelRefs = [];
500
162
  const typeObjectRefs = [];
501
163
  const otherRelevantRefs = [];
502
- for (const ref of entityRefs) {
503
- // Build byType index
504
- let typeList = byType.get(ref.type);
505
- if (!typeList) {
506
- typeList = [];
507
- byType.set(ref.type, typeList);
508
- }
509
- typeList.push(ref.expressId);
164
+ for (let i = 0; i < entityRefs.length; i++) {
165
+ if ((i & 0x3FF) === 0)
166
+ await yieldIfNeeded();
167
+ const ref = entityRefs[i];
510
168
  // Categorize (cached — .toUpperCase() called once per unique type)
511
169
  const cat = getCategory(ref.type);
170
+ const typeUpper = cat === CAT_PROPERTY_ENTITY ? getTypeUpper(ref.type) : '';
171
+ // ALL entities must be indexed in byType for on-demand extraction
172
+ // (e.g. IfcGeometricRepresentationContext, IfcSiUnit, IfcMaterialLayer).
173
+ // Only property atoms are optionally deferred for huge-file lazy loading.
174
+ const includeInPrimaryIndex = !deferPropertyAtomIndex || cat !== CAT_PROPERTY_ENTITY || PROPERTY_CONTAINER_TYPES.has(typeUpper);
175
+ if (includeInPrimaryIndex) {
176
+ let typeList = byType.get(ref.type);
177
+ if (!typeList) {
178
+ typeList = [];
179
+ byType.set(ref.type, typeList);
180
+ }
181
+ typeList.push(ref.expressId);
182
+ }
512
183
  if (cat === CAT_SPATIAL)
513
184
  spatialRefs.push(ref);
514
185
  else if (cat === CAT_GEOMETRY)
@@ -517,8 +188,12 @@ export class ColumnarParser {
517
188
  relationshipRefs.push(ref);
518
189
  else if (cat === CAT_PROPERTY_REL)
519
190
  propertyRelRefs.push(ref);
520
- else if (cat === CAT_PROPERTY_ENTITY)
521
- propertyEntityRefs.push(ref);
191
+ else if (cat === CAT_PROPERTY_ENTITY) {
192
+ if (PROPERTY_CONTAINER_TYPES.has(typeUpper))
193
+ propertyContainerRefs.push(ref);
194
+ else
195
+ propertyAtomRefs.push(ref);
196
+ }
522
197
  else if (cat === CAT_ASSOCIATION_REL)
523
198
  associationRelRefs.push(ref);
524
199
  else if (cat === CAT_TYPE_OBJECT)
@@ -526,7 +201,49 @@ export class ColumnarParser {
526
201
  else if (cat === CAT_RELEVANT)
527
202
  otherRelevantRefs.push(ref);
528
203
  }
529
- logPhase(`categorize ${totalEntities} → spatial:${spatialRefs.length} geom:${geometryRefs.length} rel:${relationshipRefs.length} propRel:${propertyRelRefs.length} assocRel:${associationRelRefs.length} type:${typeObjectRefs.length} other:${otherRelevantRefs.length}`);
204
+ 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} other:${otherRelevantRefs.length}`);
205
+ // Pre-scan association rels to discover relatingRef target IDs (e.g.
206
+ // IfcClassificationReference, IfcMaterial, IfcDocumentReference). These
207
+ // entities are typically categorised as CAT_SKIP and would otherwise be
208
+ // missing from the compact index, making on-demand extraction fail.
209
+ const associationTargetIds = new Set();
210
+ for (const ref of associationRelRefs) {
211
+ const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
212
+ if (result)
213
+ associationTargetIds.add(result.relatingDef);
214
+ }
215
+ // Collect EntityRefs for association targets that aren't already categorised.
216
+ // Single O(n) pass over entityRefs filtered to the (small) target ID set.
217
+ const alreadyIndexedIds = new Set();
218
+ for (const arr of [spatialRefs, geometryRefs, relationshipRefs, propertyRelRefs,
219
+ propertyContainerRefs, associationRelRefs, typeObjectRefs, otherRelevantRefs,
220
+ ...(deferPropertyAtomIndex ? [] : [propertyAtomRefs])]) {
221
+ for (const r of arr)
222
+ alreadyIndexedIds.add(r.expressId);
223
+ }
224
+ const extraAssocRefs = [];
225
+ for (const ref of entityRefs) {
226
+ if (associationTargetIds.has(ref.expressId) && !alreadyIndexedIds.has(ref.expressId)) {
227
+ extraAssocRefs.push(ref);
228
+ }
229
+ }
230
+ logPhase(`association target pre-scan: ${associationTargetIds.size} targets, ${extraAssocRefs.length} extra refs`);
231
+ // ALL entity refs must be indexed in byId so that on-demand extraction
232
+ // can look up any entity by expressId (e.g. IfcUnitAssignment,
233
+ // IfcGeometricRepresentationContext, IfcSiUnit, IfcLocalPlacement, etc.).
234
+ // Only property atoms are optionally deferred for huge-file lazy loading.
235
+ const indexedRefs = deferPropertyAtomIndex
236
+ ? entityRefs.filter(ref => {
237
+ const cat = getCategory(ref.type);
238
+ return cat !== CAT_PROPERTY_ENTITY || PROPERTY_CONTAINER_TYPES.has(getTypeUpper(ref.type));
239
+ })
240
+ : entityRefs;
241
+ emitDiagnostic(`index input: indexedRefs=${indexedRefs.length} deferredPropertyAtoms=${deferPropertyAtomIndex ? propertyAtomRefs.length : 0} extraAssocTargets=${extraAssocRefs.length}`);
242
+ // Build compact entity index from only the refs that survive lite parsing.
243
+ // This avoids spending huge-file startup time indexing millions of skipped
244
+ // representation/helper entities that the viewer never queries.
245
+ const compactByIdIndex = await buildCompactEntityIndexAsync(indexedRefs);
246
+ logPhase('compact entity index');
530
247
  // Create entity table builder with EXACT capacity (not totalEntities which
531
248
  // includes millions of geometry-representation entities we don't store).
532
249
  // For a 14M entity file, this reduces allocation from ~546MB to ~20MB.
@@ -537,19 +254,6 @@ export class ColumnarParser {
537
254
  byId: compactByIdIndex,
538
255
  byType,
539
256
  };
540
- // Time-based yielding: yield to the main thread every ~80ms so geometry
541
- // streaming callbacks can fire. This limits main-thread blocking to short
542
- // bursts that don't starve geometry, while adding minimal overhead (~15 yields
543
- // × ~1ms each ≈ 15ms total over the full parse).
544
- const YIELD_INTERVAL_MS = 80;
545
- let lastYieldTime = performance.now();
546
- const yieldIfNeeded = async () => {
547
- const now = performance.now();
548
- if (now - lastYieldTime >= YIELD_INTERVAL_MS) {
549
- await new Promise(resolve => setTimeout(resolve, 0));
550
- lastYieldTime = performance.now();
551
- }
552
- };
553
257
  // === TARGETED PARSING using batch byte-level extraction ===
554
258
  // Uses 2 TextDecoder.decode() calls total for ALL entity GlobalIds/Names
555
259
  // (instead of per-entity calls), and pure byte scanning for relationships.
@@ -571,28 +275,21 @@ export class ColumnarParser {
571
275
  await yieldIfNeeded();
572
276
  // Geometry + type object entities: batch extract GlobalId+Name with 2 TextDecoder calls
573
277
  options.onProgress?.({ phase: 'parsing geometry names', percent: 12 });
574
- const geomData = batchExtractGlobalIdAndName(uint8Buffer, geometryRefs);
278
+ const geomData = await batchExtractGlobalIdAndName(uint8Buffer, geometryRefs, yieldIfNeeded);
575
279
  for (const [id, data] of geomData)
576
280
  parsedEntityData.set(id, data);
577
281
  await yieldIfNeeded();
578
- const typeData = batchExtractGlobalIdAndName(uint8Buffer, typeObjectRefs);
282
+ const typeData = await batchExtractGlobalIdAndName(uint8Buffer, typeObjectRefs, yieldIfNeeded);
579
283
  for (const [id, data] of typeData)
580
284
  parsedEntityData.set(id, data);
581
285
  logPhase('batch geom GlobalId+Name');
582
286
  await yieldIfNeeded();
583
287
  // Relationships: byte-level scanning (numbers only, no TextDecoder)
584
288
  options.onProgress?.({ phase: 'parsing relationships', percent: 20 });
585
- // Use a toUpperCase cache across relationship refs (same type name set)
586
- const typeUpperCache = new Map();
587
- const getTypeUpper = (type) => {
588
- let u = typeUpperCache.get(type);
589
- if (u === undefined) {
590
- u = type.toUpperCase();
591
- typeUpperCache.set(type, u);
592
- }
593
- return u;
594
- };
595
- for (const ref of relationshipRefs) {
289
+ for (let i = 0; i < relationshipRefs.length; i++) {
290
+ if ((i & 0x3FF) === 0)
291
+ await yieldIfNeeded();
292
+ const ref = relationshipRefs[i];
596
293
  const typeUpper = getTypeUpper(ref.type);
597
294
  const rel = extractRelFast(uint8Buffer, ref.byteOffset, ref.byteLength, typeUpper);
598
295
  if (rel) {
@@ -677,7 +374,7 @@ export class ColumnarParser {
677
374
  // This replaces 252K binary searches on the 14M compact entity index with O(1) Set lookups.
678
375
  const propertySetIds = new Set();
679
376
  const quantitySetIds = new Set();
680
- for (const ref of propertyEntityRefs) {
377
+ for (const ref of propertyContainerRefs) {
681
378
  const tu = getTypeUpper(ref.type);
682
379
  if (tu === 'IFCPROPERTYSET')
683
380
  propertySetIds.add(ref.expressId);
@@ -713,14 +410,16 @@ export class ColumnarParser {
713
410
  }
714
411
  }
715
412
  }
716
- console.log(`[parseLite] propertyRels: ${propertyRelRefs.length} rels, ${totalPropRelObjects} total relatedObjects`);
717
413
  await yieldIfNeeded();
718
414
  // Association rels: byte-level scanning, no addEdge (same reasoning as property rels)
719
415
  options.onProgress?.({ phase: 'parsing associations', percent: 95 });
720
416
  const onDemandClassificationMap = new Map();
721
417
  const onDemandMaterialMap = new Map();
722
418
  const onDemandDocumentMap = new Map();
723
- for (const ref of associationRelRefs) {
419
+ for (let i = 0; i < associationRelRefs.length; i++) {
420
+ if ((i & 0x3FF) === 0)
421
+ await yieldIfNeeded();
422
+ const ref = associationRelRefs[i];
724
423
  const result = extractPropertyRelFast(uint8Buffer, ref.byteOffset, ref.byteLength);
725
424
  if (result) {
726
425
  const { relatedObjects, relatingDef: relatingRef } = result;
@@ -759,12 +458,19 @@ export class ColumnarParser {
759
458
  // Rebuild relationship graph with ALL edges (hierarchy + property + association)
760
459
  const fullRelationshipGraph = relationshipGraphBuilder.build();
761
460
  logPhase('relationship graph build()');
461
+ let deferredEntityIndex;
462
+ if (deferPropertyAtomIndex && propertyAtomRefs.length > 0) {
463
+ options.onProgress?.({ phase: 'indexing property atoms', percent: 98 });
464
+ deferredEntityIndex = await buildCompactEntityIndexAsync(propertyAtomRefs, undefined, 1024, 2);
465
+ logPhase('deferred property atom index');
466
+ }
762
467
  const parseTime = performance.now() - startTime;
763
468
  options.onProgress?.({ phase: 'complete', percent: 100 });
764
469
  return {
765
470
  ...earlyStore,
766
471
  parseTime,
767
472
  relationships: fullRelationshipGraph,
473
+ deferredEntityIndex,
768
474
  onDemandPropertyMap,
769
475
  onDemandQuantityMap,
770
476
  onDemandClassificationMap,
@@ -778,7 +484,7 @@ export class ColumnarParser {
778
484
  */
779
485
  extractPropertiesOnDemand(store, entityId) {
780
486
  // Use on-demand extraction if map is available (preferred for single-entity access)
781
- if (!store.onDemandPropertyMap) {
487
+ if (!store.onDemandPropertyMap || !store.source?.length) {
782
488
  // Fallback to pre-computed property table (e.g., server-parsed data)
783
489
  return store.properties.getForEntity(entityId);
784
490
  }
@@ -789,7 +495,7 @@ export class ColumnarParser {
789
495
  const extractor = new EntityExtractor(store.source);
790
496
  const result = [];
791
497
  for (const psetId of psetIds) {
792
- const psetRef = store.entityIndex.byId.get(psetId);
498
+ const psetRef = getEntityRefFromStore(store, psetId);
793
499
  if (!psetRef)
794
500
  continue;
795
501
  const psetEntity = extractor.extractEntity(psetRef);
@@ -804,7 +510,7 @@ export class ColumnarParser {
804
510
  for (const propRef of hasProperties) {
805
511
  if (typeof propRef !== 'number')
806
512
  continue;
807
- const propEntityRef = store.entityIndex.byId.get(propRef);
513
+ const propEntityRef = getEntityRefFromStore(store, propRef);
808
514
  if (!propEntityRef)
809
515
  continue;
810
516
  const propEntity = extractor.extractEntity(propEntityRef);
@@ -830,7 +536,7 @@ export class ColumnarParser {
830
536
  */
831
537
  extractQuantitiesOnDemand(store, entityId) {
832
538
  // Use on-demand extraction if map is available (preferred for single-entity access)
833
- if (!store.onDemandQuantityMap) {
539
+ if (!store.onDemandQuantityMap || !store.source?.length) {
834
540
  // Fallback to pre-computed quantity table (e.g., server-parsed data)
835
541
  return store.quantities.getForEntity(entityId);
836
542
  }
@@ -841,7 +547,7 @@ export class ColumnarParser {
841
547
  const extractor = new EntityExtractor(store.source);
842
548
  const result = [];
843
549
  for (const qsetId of qsetIds) {
844
- const qsetRef = store.entityIndex.byId.get(qsetId);
550
+ const qsetRef = getEntityRefFromStore(store, qsetId);
845
551
  if (!qsetRef)
846
552
  continue;
847
553
  const qsetEntity = extractor.extractEntity(qsetRef);
@@ -855,7 +561,7 @@ export class ColumnarParser {
855
561
  for (const qtyRef of hasQuantities) {
856
562
  if (typeof qtyRef !== 'number')
857
563
  continue;
858
- const qtyEntityRef = store.entityIndex.byId.get(qtyRef);
564
+ const qtyEntityRef = getEntityRefFromStore(store, qtyRef);
859
565
  if (!qtyEntityRef)
860
566
  continue;
861
567
  const qtyEntity = extractor.extractEntity(qtyEntityRef);
@@ -896,6 +602,9 @@ export function extractQuantitiesOnDemand(store, entityId) {
896
602
  const parser = new ColumnarParser();
897
603
  return parser.extractQuantitiesOnDemand(store, entityId);
898
604
  }
605
+ function getEntityRefFromStore(store, expressId) {
606
+ return store.entityIndex.byId.get(expressId) ?? store.deferredEntityIndex?.get(expressId);
607
+ }
899
608
  /**
900
609
  * Extract entity attributes on-demand from source buffer
901
610
  * Returns globalId, name, description, objectType, tag for any IfcRoot-derived entity.