@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.
- package/dist/classification-extractor.d.ts +1 -1
- package/dist/classification-extractor.d.ts.map +1 -1
- package/dist/classification-resolver.d.ts +18 -0
- package/dist/classification-resolver.d.ts.map +1 -0
- package/dist/classification-resolver.js +126 -0
- package/dist/classification-resolver.js.map +1 -0
- package/dist/columnar-parser-attributes.d.ts +39 -0
- package/dist/columnar-parser-attributes.d.ts.map +1 -0
- package/dist/columnar-parser-attributes.js +225 -0
- package/dist/columnar-parser-attributes.js.map +1 -0
- package/dist/columnar-parser-indexes.d.ts +42 -0
- package/dist/columnar-parser-indexes.d.ts.map +1 -0
- package/dist/columnar-parser-indexes.js +102 -0
- package/dist/columnar-parser-indexes.js.map +1 -0
- package/dist/columnar-parser-relationships.d.ts +17 -0
- package/dist/columnar-parser-relationships.d.ts.map +1 -0
- package/dist/columnar-parser-relationships.js +95 -0
- package/dist/columnar-parser-relationships.js.map +1 -0
- package/dist/columnar-parser.d.ts +6 -21
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +154 -445
- package/dist/columnar-parser.js.map +1 -1
- package/dist/compact-entity-index.d.ts +1 -0
- package/dist/compact-entity-index.d.ts.map +1 -1
- package/dist/compact-entity-index.js +52 -0
- package/dist/compact-entity-index.js.map +1 -1
- package/dist/georef-extractor.d.ts +7 -1
- package/dist/georef-extractor.d.ts.map +1 -1
- package/dist/georef-extractor.js +29 -5
- package/dist/georef-extractor.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -17
- package/dist/index.js.map +1 -1
- package/dist/material-extractor.d.ts +1 -1
- package/dist/material-extractor.d.ts.map +1 -1
- package/dist/material-resolver.d.ts +37 -0
- package/dist/material-resolver.d.ts.map +1 -0
- package/dist/material-resolver.js +230 -0
- package/dist/material-resolver.js.map +1 -0
- package/dist/on-demand-extractors.d.ts +4 -51
- package/dist/on-demand-extractors.d.ts.map +1 -1
- package/dist/on-demand-extractors.js +17 -341
- package/dist/on-demand-extractors.js.map +1 -1
- package/dist/style-extractor.js.map +1 -1
- package/dist/worker-parser.d.ts.map +1 -1
- package/dist/worker-parser.js +2 -4
- package/dist/worker-parser.js.map +1 -1
- package/package.json +1 -1
package/dist/columnar-parser.js
CHANGED
|
@@ -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 {
|
|
9
|
+
import { buildCompactEntityIndexAsync } from './compact-entity-index.js';
|
|
11
10
|
import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, } from '@ifc-lite/data';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
159
|
+
const propertyContainerRefs = [];
|
|
160
|
+
const propertyAtomRefs = [];
|
|
499
161
|
const associationRelRefs = [];
|
|
500
162
|
const typeObjectRefs = [];
|
|
501
163
|
const otherRelevantRefs = [];
|
|
502
|
-
for (
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|