@ifc-lite/parser 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +1 -1
- package/dist/classification-extractor.js +3 -3
- package/dist/classification-extractor.js.map +1 -1
- package/dist/columnar-parser.d.ts +86 -2
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +440 -173
- package/dist/columnar-parser.js.map +1 -1
- package/dist/entity-extractor.d.ts.map +1 -1
- package/dist/entity-extractor.js +23 -11
- package/dist/entity-extractor.js.map +1 -1
- package/dist/generated/entities.d.ts +3 -2
- package/dist/generated/entities.d.ts.map +1 -1
- package/dist/generated/entities.js +6 -3
- package/dist/generated/entities.js.map +1 -1
- package/dist/generated/enums.d.ts.map +1 -1
- package/dist/generated/enums.js +0 -3
- package/dist/generated/enums.js.map +1 -1
- package/dist/generated/index.d.ts +4 -2
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +4 -2
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/schema-registry.d.ts.map +1 -1
- package/dist/generated/schema-registry.js +8 -3
- package/dist/generated/schema-registry.js.map +1 -1
- package/dist/generated/selects.d.ts +3 -0
- package/dist/generated/selects.d.ts.map +1 -1
- package/dist/generated/selects.js +6 -3
- package/dist/generated/selects.js.map +1 -1
- package/dist/generated/serializers.d.ts +71 -0
- package/dist/generated/serializers.d.ts.map +1 -0
- package/dist/generated/serializers.js +236 -0
- package/dist/generated/serializers.js.map +1 -0
- package/dist/generated/test-compile.d.ts +1 -6
- package/dist/generated/test-compile.d.ts.map +1 -1
- package/dist/generated/test-compile.js +30 -25
- package/dist/generated/test-compile.js.map +1 -1
- package/dist/generated/type-ids.d.ts +815 -0
- package/dist/generated/type-ids.d.ts.map +1 -0
- package/dist/generated/type-ids.js +1669 -0
- package/dist/generated/type-ids.js.map +1 -0
- package/dist/generated/types.d.ts +9 -542
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/generated/types.js +6 -497
- package/dist/generated/types.js.map +1 -1
- package/dist/georef-extractor.js +2 -2
- package/dist/georef-extractor.js.map +1 -1
- package/dist/index.d.ts +38 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +233 -41
- package/dist/index.js.map +1 -1
- package/dist/material-extractor.js +8 -8
- package/dist/material-extractor.js.map +1 -1
- package/dist/parser.worker.d.ts +2 -0
- package/dist/parser.worker.d.ts.map +1 -0
- package/dist/parser.worker.js +43 -0
- package/dist/parser.worker.js.map +1 -0
- package/dist/property-extractor.d.ts +5 -1
- package/dist/property-extractor.d.ts.map +1 -1
- package/dist/property-extractor.js +22 -1
- package/dist/property-extractor.js.map +1 -1
- package/dist/quantity-extractor.d.ts +5 -1
- package/dist/quantity-extractor.d.ts.map +1 -1
- package/dist/quantity-extractor.js +29 -1
- package/dist/quantity-extractor.js.map +1 -1
- package/dist/relationship-extractor.d.ts +5 -1
- package/dist/relationship-extractor.d.ts.map +1 -1
- package/dist/relationship-extractor.js +27 -2
- package/dist/relationship-extractor.js.map +1 -1
- package/dist/spatial-hierarchy-builder.d.ts +4 -3
- package/dist/spatial-hierarchy-builder.d.ts.map +1 -1
- package/dist/spatial-hierarchy-builder.js +46 -34
- package/dist/spatial-hierarchy-builder.js.map +1 -1
- package/dist/style-extractor.d.ts +1 -0
- package/dist/style-extractor.d.ts.map +1 -1
- package/dist/style-extractor.js +18 -0
- package/dist/style-extractor.js.map +1 -1
- package/dist/tokenizer.d.ts +11 -0
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +150 -10
- package/dist/tokenizer.js.map +1 -1
- package/dist/unit-extractor.d.ts +22 -0
- package/dist/unit-extractor.d.ts.map +1 -0
- package/dist/unit-extractor.js +205 -0
- package/dist/unit-extractor.js.map +1 -0
- package/dist/worker-parser.d.ts +28 -0
- package/dist/worker-parser.d.ts.map +1 -0
- package/dist/worker-parser.js +81 -0
- package/dist/worker-parser.js.map +1 -0
- package/package.json +9 -8
- package/dist/examples/comprehensive-extraction.d.ts +0 -76
- package/dist/examples/comprehensive-extraction.d.ts.map +0 -1
- package/dist/examples/comprehensive-extraction.js +0 -228
- package/dist/examples/comprehensive-extraction.js.map +0 -1
package/dist/columnar-parser.js
CHANGED
|
@@ -1,216 +1,296 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
-
import { PropertyExtractor } from './property-extractor.js';
|
|
5
|
-
import { QuantityExtractor } from './quantity-extractor.js';
|
|
6
|
-
import { RelationshipExtractor } from './relationship-extractor.js';
|
|
7
4
|
import { SpatialHierarchyBuilder } from './spatial-hierarchy-builder.js';
|
|
8
|
-
import {
|
|
5
|
+
import { EntityExtractor } from './entity-extractor.js';
|
|
6
|
+
import { extractLengthUnitScale } from './unit-extractor.js';
|
|
7
|
+
import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, } from '@ifc-lite/data';
|
|
8
|
+
// Pre-computed type sets for O(1) lookups
|
|
9
|
+
const GEOMETRY_TYPES = new Set([
|
|
10
|
+
'IFCWALL', 'IFCWALLSTANDARDCASE', 'IFCDOOR', 'IFCWINDOW', 'IFCSLAB',
|
|
11
|
+
'IFCCOLUMN', 'IFCBEAM', 'IFCROOF', 'IFCSTAIR', 'IFCSTAIRFLIGHT',
|
|
12
|
+
'IFCRAILING', 'IFCRAMP', 'IFCRAMPFLIGHT', 'IFCPLATE', 'IFCMEMBER',
|
|
13
|
+
'IFCCURTAINWALL', 'IFCFOOTING', 'IFCPILE', 'IFCBUILDINGELEMENTPROXY',
|
|
14
|
+
'IFCFURNISHINGELEMENT', 'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL',
|
|
15
|
+
'IFCFLOWCONTROLLER', 'IFCFLOWFITTING', 'IFCSPACE', 'IFCOPENINGELEMENT',
|
|
16
|
+
'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
|
|
17
|
+
]);
|
|
18
|
+
// IMPORTANT: This set MUST include ALL RelationshipType enum values to prevent semantic loss
|
|
19
|
+
// Missing types will be skipped during parsing, causing incomplete relationship graphs
|
|
20
|
+
const RELATIONSHIP_TYPES = new Set([
|
|
21
|
+
'IFCRELCONTAINEDINSPATIALSTRUCTURE', 'IFCRELAGGREGATES',
|
|
22
|
+
'IFCRELDEFINESBYPROPERTIES', 'IFCRELDEFINESBYTYPE',
|
|
23
|
+
'IFCRELASSOCIATESMATERIAL', 'IFCRELASSOCIATESCLASSIFICATION',
|
|
24
|
+
'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
|
|
25
|
+
'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
|
|
26
|
+
'IFCRELSPACEBOUNDARY',
|
|
27
|
+
'IFCRELASSIGNSTOGROUP', 'IFCRELASSIGNSTOPRODUCT',
|
|
28
|
+
'IFCRELREFERENCEDINSPATIALSTRUCTURE',
|
|
29
|
+
]);
|
|
30
|
+
// Map IFC relationship type strings to RelationshipType enum
|
|
31
|
+
// MUST cover ALL RelationshipType enum values (14 types total)
|
|
32
|
+
const REL_TYPE_MAP = {
|
|
33
|
+
'IFCRELCONTAINEDINSPATIALSTRUCTURE': RelationshipType.ContainsElements,
|
|
34
|
+
'IFCRELAGGREGATES': RelationshipType.Aggregates,
|
|
35
|
+
'IFCRELDEFINESBYPROPERTIES': RelationshipType.DefinesByProperties,
|
|
36
|
+
'IFCRELDEFINESBYTYPE': RelationshipType.DefinesByType,
|
|
37
|
+
'IFCRELASSOCIATESMATERIAL': RelationshipType.AssociatesMaterial,
|
|
38
|
+
'IFCRELASSOCIATESCLASSIFICATION': RelationshipType.AssociatesClassification,
|
|
39
|
+
'IFCRELVOIDSELEMENT': RelationshipType.VoidsElement,
|
|
40
|
+
'IFCRELFILLSELEMENT': RelationshipType.FillsElement,
|
|
41
|
+
'IFCRELCONNECTSPATHELEMENTS': RelationshipType.ConnectsPathElements,
|
|
42
|
+
'IFCRELCONNECTSELEMENTS': RelationshipType.ConnectsElements,
|
|
43
|
+
'IFCRELSPACEBOUNDARY': RelationshipType.SpaceBoundary,
|
|
44
|
+
'IFCRELASSIGNSTOGROUP': RelationshipType.AssignsToGroup,
|
|
45
|
+
'IFCRELASSIGNSTOPRODUCT': RelationshipType.AssignsToProduct,
|
|
46
|
+
'IFCRELREFERENCEDINSPATIALSTRUCTURE': RelationshipType.ReferencedInSpatialStructure,
|
|
47
|
+
};
|
|
48
|
+
const QUANTITY_TYPE_MAP = {
|
|
49
|
+
'IFCQUANTITYLENGTH': QuantityType.Length,
|
|
50
|
+
'IFCQUANTITYAREA': QuantityType.Area,
|
|
51
|
+
'IFCQUANTITYVOLUME': QuantityType.Volume,
|
|
52
|
+
'IFCQUANTITYCOUNT': QuantityType.Count,
|
|
53
|
+
'IFCQUANTITYWEIGHT': QuantityType.Weight,
|
|
54
|
+
'IFCQUANTITYTIME': QuantityType.Time,
|
|
55
|
+
};
|
|
56
|
+
// Types needed for spatial hierarchy (small subset)
|
|
57
|
+
const SPATIAL_TYPES = new Set([
|
|
58
|
+
'IFCPROJECT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY', 'IFCSPACE',
|
|
59
|
+
]);
|
|
60
|
+
// Relationship types needed for hierarchy
|
|
61
|
+
const HIERARCHY_REL_TYPES = new Set([
|
|
62
|
+
'IFCRELAGGREGATES', 'IFCRELCONTAINEDINSPATIALSTRUCTURE',
|
|
63
|
+
]);
|
|
64
|
+
// Relationship types for on-demand property loading
|
|
65
|
+
const PROPERTY_REL_TYPES = new Set([
|
|
66
|
+
'IFCRELDEFINESBYPROPERTIES',
|
|
67
|
+
]);
|
|
68
|
+
// Property-related entity types for on-demand extraction
|
|
69
|
+
const PROPERTY_ENTITY_TYPES = new Set([
|
|
70
|
+
'IFCPROPERTYSET', 'IFCELEMENTQUANTITY',
|
|
71
|
+
'IFCPROPERTYSINGLEVALUE', 'IFCPROPERTYENUMERATEDVALUE',
|
|
72
|
+
'IFCPROPERTYBOUNDEDVALUE', 'IFCPROPERTYTABLEVALUE',
|
|
73
|
+
'IFCPROPERTYLISTVALUE', 'IFCPROPERTYREFERENCEVALUE',
|
|
74
|
+
'IFCQUANTITYLENGTH', 'IFCQUANTITYAREA', 'IFCQUANTITYVOLUME',
|
|
75
|
+
'IFCQUANTITYCOUNT', 'IFCQUANTITYWEIGHT', 'IFCQUANTITYTIME',
|
|
76
|
+
]);
|
|
77
|
+
// Yield helper - batched to reduce overhead
|
|
78
|
+
const YIELD_INTERVAL = 5000;
|
|
79
|
+
let yieldCounter = 0;
|
|
80
|
+
async function maybeYield() {
|
|
81
|
+
yieldCounter++;
|
|
82
|
+
if (yieldCounter >= YIELD_INTERVAL) {
|
|
83
|
+
yieldCounter = 0;
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
9
87
|
export class ColumnarParser {
|
|
10
88
|
/**
|
|
11
89
|
* Parse IFC file into columnar data store
|
|
90
|
+
*
|
|
91
|
+
* Uses fast semicolon-based scanning with on-demand property extraction.
|
|
92
|
+
* Properties are parsed lazily when accessed, not upfront.
|
|
93
|
+
* This provides instant UI responsiveness even for very large files.
|
|
12
94
|
*/
|
|
13
|
-
async
|
|
95
|
+
async parseLite(buffer, entityRefs, options = {}) {
|
|
14
96
|
const startTime = performance.now();
|
|
15
97
|
const uint8Buffer = new Uint8Array(buffer);
|
|
98
|
+
const totalEntities = entityRefs.length;
|
|
99
|
+
options.onProgress?.({ phase: 'building', percent: 0 });
|
|
16
100
|
// Initialize builders
|
|
17
101
|
const strings = new StringTable();
|
|
18
|
-
const entityTableBuilder = new EntityTableBuilder(
|
|
102
|
+
const entityTableBuilder = new EntityTableBuilder(totalEntities, strings);
|
|
19
103
|
const propertyTableBuilder = new PropertyTableBuilder(strings);
|
|
104
|
+
const quantityTableBuilder = new QuantityTableBuilder(strings);
|
|
20
105
|
const relationshipGraphBuilder = new RelationshipGraphBuilder();
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
106
|
+
// Build entity index early (needed for property relationship lookup)
|
|
107
|
+
const entityIndex = {
|
|
108
|
+
byId: new Map(),
|
|
109
|
+
byType: new Map(),
|
|
110
|
+
};
|
|
111
|
+
// First pass: collect spatial, relationship, and property refs for targeted parsing
|
|
112
|
+
const spatialRefs = [];
|
|
113
|
+
const relationshipRefs = [];
|
|
114
|
+
const propertyRelRefs = [];
|
|
115
|
+
const propertyEntityRefs = [];
|
|
116
|
+
for (const ref of entityRefs) {
|
|
117
|
+
// Build entity index
|
|
118
|
+
entityIndex.byId.set(ref.expressId, ref);
|
|
119
|
+
let typeList = entityIndex.byType.get(ref.type);
|
|
120
|
+
if (!typeList) {
|
|
121
|
+
typeList = [];
|
|
122
|
+
entityIndex.byType.set(ref.type, typeList);
|
|
123
|
+
}
|
|
124
|
+
typeList.push(ref.expressId);
|
|
125
|
+
// Categorize refs for targeted parsing
|
|
126
|
+
const typeUpper = ref.type.toUpperCase();
|
|
127
|
+
if (SPATIAL_TYPES.has(typeUpper)) {
|
|
128
|
+
spatialRefs.push(ref);
|
|
129
|
+
}
|
|
130
|
+
else if (HIERARCHY_REL_TYPES.has(typeUpper)) {
|
|
131
|
+
relationshipRefs.push(ref);
|
|
132
|
+
}
|
|
133
|
+
else if (PROPERTY_REL_TYPES.has(typeUpper)) {
|
|
134
|
+
propertyRelRefs.push(ref);
|
|
135
|
+
}
|
|
136
|
+
else if (PROPERTY_ENTITY_TYPES.has(typeUpper)) {
|
|
137
|
+
propertyEntityRefs.push(ref);
|
|
42
138
|
}
|
|
43
139
|
}
|
|
44
|
-
|
|
45
|
-
options.onProgress?.({ phase: '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const psetId = rel.relatingObject;
|
|
57
|
-
for (const entityId of rel.relatedObjects) {
|
|
58
|
-
let list = psetToEntities.get(psetId);
|
|
59
|
-
if (!list) {
|
|
60
|
-
list = [];
|
|
61
|
-
psetToEntities.set(psetId, list);
|
|
62
|
-
}
|
|
63
|
-
list.push(entityId);
|
|
64
|
-
}
|
|
140
|
+
// === TARGETED PARSING: Parse spatial entities first ===
|
|
141
|
+
options.onProgress?.({ phase: 'parsing spatial', percent: 10 });
|
|
142
|
+
const extractor = new EntityExtractor(uint8Buffer);
|
|
143
|
+
const parsedSpatialData = new Map();
|
|
144
|
+
// Parse spatial entities (typically < 100 entities)
|
|
145
|
+
for (const ref of spatialRefs) {
|
|
146
|
+
const entity = extractor.extractEntity(ref);
|
|
147
|
+
if (entity) {
|
|
148
|
+
const attrs = entity.attributes || [];
|
|
149
|
+
const globalId = typeof attrs[0] === 'string' ? attrs[0] : '';
|
|
150
|
+
const name = typeof attrs[2] === 'string' ? attrs[2] : '';
|
|
151
|
+
parsedSpatialData.set(ref.expressId, { globalId, name });
|
|
65
152
|
}
|
|
66
153
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
propType = PropertyValueType.String;
|
|
85
|
-
value = String(propValue.value);
|
|
154
|
+
console.log(`[ColumnarParser] Parsed ${spatialRefs.length} spatial entities`);
|
|
155
|
+
// Parse relationship entities (typically < 10k entities)
|
|
156
|
+
options.onProgress?.({ phase: 'parsing relationships', percent: 20 });
|
|
157
|
+
const relationships = [];
|
|
158
|
+
for (const ref of relationshipRefs) {
|
|
159
|
+
const entity = extractor.extractEntity(ref);
|
|
160
|
+
if (entity) {
|
|
161
|
+
const typeUpper = entity.type.toUpperCase();
|
|
162
|
+
const rel = this.extractRelationshipFast(entity, typeUpper);
|
|
163
|
+
if (rel) {
|
|
164
|
+
relationships.push(rel);
|
|
165
|
+
// Add to relationship graph
|
|
166
|
+
const relType = REL_TYPE_MAP[typeUpper];
|
|
167
|
+
if (relType) {
|
|
168
|
+
for (const targetId of rel.relatedObjects) {
|
|
169
|
+
relationshipGraphBuilder.addEdge(rel.relatingObject, targetId, relType, rel.relatingObject);
|
|
170
|
+
}
|
|
86
171
|
}
|
|
87
|
-
propertyTableBuilder.add({
|
|
88
|
-
entityId,
|
|
89
|
-
psetName: pset.name,
|
|
90
|
-
psetGlobalId: globalId,
|
|
91
|
-
propName,
|
|
92
|
-
propType,
|
|
93
|
-
value,
|
|
94
|
-
});
|
|
95
172
|
}
|
|
96
173
|
}
|
|
97
174
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
175
|
+
console.log(`[ColumnarParser] Parsed ${relationshipRefs.length} relationship entities, ${relationships.length} valid relationships`);
|
|
176
|
+
// === PARSE PROPERTY RELATIONSHIPS for on-demand loading ===
|
|
177
|
+
options.onProgress?.({ phase: 'parsing property refs', percent: 25 });
|
|
178
|
+
const onDemandPropertyMap = new Map();
|
|
179
|
+
const onDemandQuantityMap = new Map();
|
|
180
|
+
// Parse IfcRelDefinesByProperties to build entity -> pset/qset mapping
|
|
181
|
+
// ALSO add to relationship graph so cache loads can rebuild on-demand maps
|
|
182
|
+
for (const ref of propertyRelRefs) {
|
|
183
|
+
const entity = extractor.extractEntity(ref);
|
|
184
|
+
if (entity) {
|
|
185
|
+
const attrs = entity.attributes || [];
|
|
186
|
+
// IfcRelDefinesByProperties: relatedObjects at [4], relatingPropertyDefinition at [5]
|
|
187
|
+
const relatedObjects = attrs[4];
|
|
188
|
+
const relatingDef = attrs[5];
|
|
189
|
+
if (typeof relatingDef === 'number' && Array.isArray(relatedObjects)) {
|
|
190
|
+
// Add to relationship graph (needed for cache rebuild)
|
|
191
|
+
for (const objId of relatedObjects) {
|
|
192
|
+
if (typeof objId === 'number') {
|
|
193
|
+
relationshipGraphBuilder.addEdge(relatingDef, objId, RelationshipType.DefinesByProperties, ref.expressId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Find if the relating definition is a property set or quantity set
|
|
197
|
+
const defRef = entityIndex.byId.get(relatingDef);
|
|
198
|
+
if (defRef) {
|
|
199
|
+
const defTypeUpper = defRef.type.toUpperCase();
|
|
200
|
+
const isPropertySet = defTypeUpper === 'IFCPROPERTYSET';
|
|
201
|
+
const isQuantitySet = defTypeUpper === 'IFCELEMENTQUANTITY';
|
|
202
|
+
if (isPropertySet || isQuantitySet) {
|
|
203
|
+
const targetMap = isPropertySet ? onDemandPropertyMap : onDemandQuantityMap;
|
|
204
|
+
for (const objId of relatedObjects) {
|
|
205
|
+
if (typeof objId === 'number') {
|
|
206
|
+
let list = targetMap.get(objId);
|
|
207
|
+
if (!list) {
|
|
208
|
+
list = [];
|
|
209
|
+
targetMap.set(objId, list);
|
|
210
|
+
}
|
|
211
|
+
list.push(relatingDef);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
117
214
|
}
|
|
118
|
-
list.push(entityId);
|
|
119
215
|
}
|
|
120
216
|
}
|
|
121
217
|
}
|
|
122
218
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
219
|
+
console.log(`[ColumnarParser] On-demand: ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities`);
|
|
220
|
+
// === BUILD ENTITY TABLE with spatial data included ===
|
|
221
|
+
options.onProgress?.({ phase: 'building entities', percent: 30 });
|
|
222
|
+
// OPTIMIZATION: Only add entities that are useful for the viewer UI
|
|
223
|
+
// Skip geometric primitives like IFCCARTESIANPOINT, IFCDIRECTION, etc.
|
|
224
|
+
// This reduces 4M+ entities to ~100K relevant ones
|
|
225
|
+
const RELEVANT_ENTITY_PREFIXES = new Set([
|
|
226
|
+
'IFCWALL', 'IFCSLAB', 'IFCBEAM', 'IFCCOLUMN', 'IFCPLATE', 'IFCDOOR', 'IFCWINDOW',
|
|
227
|
+
'IFCROOF', 'IFCSTAIR', 'IFCRAILING', 'IFCRAMP', 'IFCFOOTING', 'IFCPILE',
|
|
228
|
+
'IFCMEMBER', 'IFCCURTAINWALL', 'IFCBUILDINGELEMENTPROXY', 'IFCFURNISHINGELEMENT',
|
|
229
|
+
'IFCFLOWSEGMENT', 'IFCFLOWTERMINAL', 'IFCFLOWCONTROLLER', 'IFCFLOWFITTING',
|
|
230
|
+
'IFCSPACE', 'IFCOPENINGELEMENT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY',
|
|
231
|
+
'IFCPROJECT', 'IFCCOVERING', 'IFCANNOTATION', 'IFCGRID',
|
|
232
|
+
]);
|
|
233
|
+
let processed = 0;
|
|
234
|
+
let added = 0;
|
|
235
|
+
for (const ref of entityRefs) {
|
|
236
|
+
const typeUpper = ref.type.toUpperCase();
|
|
237
|
+
// Skip non-relevant entities (geometric primitives, etc.)
|
|
238
|
+
const hasGeometry = GEOMETRY_TYPES.has(typeUpper);
|
|
239
|
+
const isType = typeUpper.endsWith('TYPE');
|
|
240
|
+
const isSpatial = SPATIAL_TYPES.has(typeUpper);
|
|
241
|
+
const isRelevant = hasGeometry || isType || isSpatial ||
|
|
242
|
+
RELEVANT_ENTITY_PREFIXES.has(typeUpper) ||
|
|
243
|
+
typeUpper.startsWith('IFCREL') || // Keep relationships for hierarchy
|
|
244
|
+
onDemandPropertyMap.has(ref.expressId) || // Keep entities with properties
|
|
245
|
+
onDemandQuantityMap.has(ref.expressId); // Keep entities with quantities
|
|
246
|
+
if (!isRelevant) {
|
|
247
|
+
processed++;
|
|
248
|
+
continue;
|
|
146
249
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'IFCRELCONNECTSPATHELEMENTS': RelationshipType.ConnectsPathElements,
|
|
162
|
-
'IFCRELSPACEBOUNDARY': RelationshipType.SpaceBoundary,
|
|
163
|
-
};
|
|
164
|
-
for (const rel of relationships) {
|
|
165
|
-
const relType = relTypeMap[rel.type.toUpperCase()];
|
|
166
|
-
if (relType) {
|
|
167
|
-
for (const targetId of rel.relatedObjects) {
|
|
168
|
-
relationshipGraphBuilder.addEdge(rel.relatingObject, targetId, relType, rel.relatingObject);
|
|
169
|
-
}
|
|
250
|
+
// Get parsed data for spatial entities
|
|
251
|
+
const spatialData = parsedSpatialData.get(ref.expressId);
|
|
252
|
+
const globalId = spatialData?.globalId || '';
|
|
253
|
+
const name = spatialData?.name || '';
|
|
254
|
+
entityTableBuilder.add(ref.expressId, ref.type, globalId, name, '', // description
|
|
255
|
+
'', // objectType
|
|
256
|
+
hasGeometry, isType);
|
|
257
|
+
added++;
|
|
258
|
+
processed++;
|
|
259
|
+
// Yield every 10000 entities for better interleaving with geometry streaming
|
|
260
|
+
if (processed % 10000 === 0) {
|
|
261
|
+
options.onProgress?.({ phase: 'building entities', percent: 30 + (processed / totalEntities) * 50 });
|
|
262
|
+
// Direct yield - don't use maybeYield since we're already throttling
|
|
263
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
170
264
|
}
|
|
171
265
|
}
|
|
266
|
+
console.log(`[ColumnarParser] Added ${added} relevant entities (skipped ${totalEntities - added} primitives)`);
|
|
267
|
+
const entityTable = entityTableBuilder.build();
|
|
268
|
+
// Empty property/quantity tables - use on-demand extraction instead
|
|
269
|
+
const propertyTable = propertyTableBuilder.build();
|
|
270
|
+
const quantityTable = quantityTableBuilder.build();
|
|
172
271
|
const relationshipGraph = relationshipGraphBuilder.build();
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
schemaVersion = 'IFC4';
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const parseTime = performance.now() - startTime;
|
|
184
|
-
// Build entity index
|
|
185
|
-
const entityIndex = {
|
|
186
|
-
byId: new Map(),
|
|
187
|
-
byType: new Map(),
|
|
188
|
-
};
|
|
189
|
-
for (const ref of entityRefs) {
|
|
190
|
-
entityIndex.byId.set(ref.expressId, ref);
|
|
191
|
-
let typeList = entityIndex.byType.get(ref.type);
|
|
192
|
-
if (!typeList) {
|
|
193
|
-
typeList = [];
|
|
194
|
-
entityIndex.byType.set(ref.type, typeList);
|
|
195
|
-
}
|
|
196
|
-
typeList.push(ref.expressId);
|
|
197
|
-
}
|
|
198
|
-
// === Build Spatial Hierarchy ===
|
|
199
|
-
options.onProgress?.({ phase: 'spatial-hierarchy', percent: 0 });
|
|
272
|
+
// === EXTRACT LENGTH UNIT SCALE ===
|
|
273
|
+
options.onProgress?.({ phase: 'extracting units', percent: 85 });
|
|
274
|
+
const lengthUnitScale = extractLengthUnitScale(uint8Buffer, entityIndex);
|
|
275
|
+
console.log(`[ColumnarParser] Length unit scale: ${lengthUnitScale}`);
|
|
276
|
+
// === BUILD SPATIAL HIERARCHY ===
|
|
277
|
+
options.onProgress?.({ phase: 'building hierarchy', percent: 90 });
|
|
200
278
|
let spatialHierarchy;
|
|
201
279
|
try {
|
|
202
280
|
const hierarchyBuilder = new SpatialHierarchyBuilder();
|
|
203
|
-
spatialHierarchy = hierarchyBuilder.build(entityTable, relationshipGraph, strings, uint8Buffer, entityIndex);
|
|
281
|
+
spatialHierarchy = hierarchyBuilder.build(entityTable, relationshipGraph, strings, uint8Buffer, entityIndex, lengthUnitScale);
|
|
282
|
+
console.log(`[ColumnarParser] Built spatial hierarchy with ${spatialHierarchy.byStorey.size} storeys`);
|
|
204
283
|
}
|
|
205
284
|
catch (error) {
|
|
206
285
|
console.warn('[ColumnarParser] Failed to build spatial hierarchy:', error);
|
|
207
|
-
// Continue without hierarchy - it's optional
|
|
208
286
|
}
|
|
209
|
-
|
|
287
|
+
const parseTime = performance.now() - startTime;
|
|
288
|
+
console.log(`[ColumnarParser] Parsed ${totalEntities} entities in ${parseTime.toFixed(0)}ms`);
|
|
289
|
+
options.onProgress?.({ phase: 'complete', percent: 100 });
|
|
210
290
|
return {
|
|
211
291
|
fileSize: buffer.byteLength,
|
|
212
|
-
schemaVersion,
|
|
213
|
-
entityCount:
|
|
292
|
+
schemaVersion: 'IFC4',
|
|
293
|
+
entityCount: totalEntities,
|
|
214
294
|
parseTime,
|
|
215
295
|
source: uint8Buffer,
|
|
216
296
|
entityIndex,
|
|
@@ -220,7 +300,194 @@ export class ColumnarParser {
|
|
|
220
300
|
quantities: quantityTable,
|
|
221
301
|
relationships: relationshipGraph,
|
|
222
302
|
spatialHierarchy,
|
|
303
|
+
onDemandPropertyMap, // For instant property access
|
|
304
|
+
onDemandQuantityMap, // For instant quantity access
|
|
223
305
|
};
|
|
224
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Fast relationship extraction - inline for performance
|
|
309
|
+
*/
|
|
310
|
+
extractRelationshipFast(entity, typeUpper) {
|
|
311
|
+
const attrs = entity.attributes;
|
|
312
|
+
if (attrs.length < 6)
|
|
313
|
+
return null;
|
|
314
|
+
let relatingObject;
|
|
315
|
+
let relatedObjects;
|
|
316
|
+
if (typeUpper === 'IFCRELDEFINESBYPROPERTIES' || typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE') {
|
|
317
|
+
relatedObjects = attrs[4];
|
|
318
|
+
relatingObject = attrs[5];
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
relatingObject = attrs[4];
|
|
322
|
+
relatedObjects = attrs[5];
|
|
323
|
+
}
|
|
324
|
+
if (typeof relatingObject !== 'number' || !Array.isArray(relatedObjects)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
type: entity.type,
|
|
329
|
+
relatingObject,
|
|
330
|
+
relatedObjects: relatedObjects.filter((id) => typeof id === 'number'),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Extract properties for a single entity ON-DEMAND
|
|
335
|
+
* Parses only what's needed from the source buffer - instant results.
|
|
336
|
+
*/
|
|
337
|
+
extractPropertiesOnDemand(store, entityId) {
|
|
338
|
+
// Use on-demand extraction if map is available (preferred for single-entity access)
|
|
339
|
+
if (!store.onDemandPropertyMap) {
|
|
340
|
+
// Fallback to pre-computed property table (e.g., server-parsed data)
|
|
341
|
+
return store.properties.getForEntity(entityId);
|
|
342
|
+
}
|
|
343
|
+
const psetIds = store.onDemandPropertyMap.get(entityId);
|
|
344
|
+
if (!psetIds || psetIds.length === 0) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
const extractor = new EntityExtractor(store.source);
|
|
348
|
+
const result = [];
|
|
349
|
+
for (const psetId of psetIds) {
|
|
350
|
+
const psetRef = store.entityIndex.byId.get(psetId);
|
|
351
|
+
if (!psetRef)
|
|
352
|
+
continue;
|
|
353
|
+
const psetEntity = extractor.extractEntity(psetRef);
|
|
354
|
+
if (!psetEntity)
|
|
355
|
+
continue;
|
|
356
|
+
const psetAttrs = psetEntity.attributes || [];
|
|
357
|
+
const psetGlobalId = typeof psetAttrs[0] === 'string' ? psetAttrs[0] : undefined;
|
|
358
|
+
const psetName = typeof psetAttrs[2] === 'string' ? psetAttrs[2] : `PropertySet #${psetId}`;
|
|
359
|
+
const hasProperties = psetAttrs[4];
|
|
360
|
+
const properties = [];
|
|
361
|
+
if (Array.isArray(hasProperties)) {
|
|
362
|
+
for (const propRef of hasProperties) {
|
|
363
|
+
if (typeof propRef !== 'number')
|
|
364
|
+
continue;
|
|
365
|
+
const propEntityRef = store.entityIndex.byId.get(propRef);
|
|
366
|
+
if (!propEntityRef)
|
|
367
|
+
continue;
|
|
368
|
+
const propEntity = extractor.extractEntity(propEntityRef);
|
|
369
|
+
if (!propEntity)
|
|
370
|
+
continue;
|
|
371
|
+
const propAttrs = propEntity.attributes || [];
|
|
372
|
+
const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
|
|
373
|
+
if (!propName)
|
|
374
|
+
continue;
|
|
375
|
+
// IfcPropertySingleValue: [name, description, nominalValue, unit]
|
|
376
|
+
const nominalValue = propAttrs[2];
|
|
377
|
+
let type = 0; // String
|
|
378
|
+
let value = nominalValue;
|
|
379
|
+
if (typeof nominalValue === 'number') {
|
|
380
|
+
type = 1; // Real
|
|
381
|
+
}
|
|
382
|
+
else if (typeof nominalValue === 'boolean') {
|
|
383
|
+
type = 2; // Boolean
|
|
384
|
+
}
|
|
385
|
+
else if (nominalValue !== null && nominalValue !== undefined) {
|
|
386
|
+
value = String(nominalValue);
|
|
387
|
+
}
|
|
388
|
+
properties.push({ name: propName, type, value });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (properties.length > 0 || psetName) {
|
|
392
|
+
result.push({ name: psetName, globalId: psetGlobalId, properties });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extract quantities for a single entity ON-DEMAND
|
|
399
|
+
* Parses only what's needed from the source buffer - instant results.
|
|
400
|
+
*/
|
|
401
|
+
extractQuantitiesOnDemand(store, entityId) {
|
|
402
|
+
// Use on-demand extraction if map is available (preferred for single-entity access)
|
|
403
|
+
if (!store.onDemandQuantityMap) {
|
|
404
|
+
// Fallback to pre-computed quantity table (e.g., server-parsed data)
|
|
405
|
+
return store.quantities.getForEntity(entityId);
|
|
406
|
+
}
|
|
407
|
+
const qsetIds = store.onDemandQuantityMap.get(entityId);
|
|
408
|
+
if (!qsetIds || qsetIds.length === 0) {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
const extractor = new EntityExtractor(store.source);
|
|
412
|
+
const result = [];
|
|
413
|
+
for (const qsetId of qsetIds) {
|
|
414
|
+
const qsetRef = store.entityIndex.byId.get(qsetId);
|
|
415
|
+
if (!qsetRef)
|
|
416
|
+
continue;
|
|
417
|
+
const qsetEntity = extractor.extractEntity(qsetRef);
|
|
418
|
+
if (!qsetEntity)
|
|
419
|
+
continue;
|
|
420
|
+
const qsetAttrs = qsetEntity.attributes || [];
|
|
421
|
+
const qsetName = typeof qsetAttrs[2] === 'string' ? qsetAttrs[2] : `QuantitySet #${qsetId}`;
|
|
422
|
+
const hasQuantities = qsetAttrs[5];
|
|
423
|
+
const quantities = [];
|
|
424
|
+
if (Array.isArray(hasQuantities)) {
|
|
425
|
+
for (const qtyRef of hasQuantities) {
|
|
426
|
+
if (typeof qtyRef !== 'number')
|
|
427
|
+
continue;
|
|
428
|
+
const qtyEntityRef = store.entityIndex.byId.get(qtyRef);
|
|
429
|
+
if (!qtyEntityRef)
|
|
430
|
+
continue;
|
|
431
|
+
const qtyEntity = extractor.extractEntity(qtyEntityRef);
|
|
432
|
+
if (!qtyEntity)
|
|
433
|
+
continue;
|
|
434
|
+
const qtyAttrs = qtyEntity.attributes || [];
|
|
435
|
+
const qtyName = typeof qtyAttrs[0] === 'string' ? qtyAttrs[0] : '';
|
|
436
|
+
if (!qtyName)
|
|
437
|
+
continue;
|
|
438
|
+
// Get quantity type from entity type
|
|
439
|
+
const qtyTypeUpper = qtyEntity.type.toUpperCase();
|
|
440
|
+
const qtyType = QUANTITY_TYPE_MAP[qtyTypeUpper] ?? QuantityType.Count;
|
|
441
|
+
// Value is at index 3 for most quantity types
|
|
442
|
+
const value = typeof qtyAttrs[3] === 'number' ? qtyAttrs[3] : 0;
|
|
443
|
+
quantities.push({ name: qtyName, type: qtyType, value });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (quantities.length > 0 || qsetName) {
|
|
447
|
+
result.push({ name: qsetName, quantities });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Standalone on-demand property extractor
|
|
455
|
+
* Can be used outside ColumnarParser class
|
|
456
|
+
*/
|
|
457
|
+
export function extractPropertiesOnDemand(store, entityId) {
|
|
458
|
+
const parser = new ColumnarParser();
|
|
459
|
+
return parser.extractPropertiesOnDemand(store, entityId);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Standalone on-demand quantity extractor
|
|
463
|
+
* Can be used outside ColumnarParser class
|
|
464
|
+
*/
|
|
465
|
+
export function extractQuantitiesOnDemand(store, entityId) {
|
|
466
|
+
const parser = new ColumnarParser();
|
|
467
|
+
return parser.extractQuantitiesOnDemand(store, entityId);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Extract entity attributes on-demand from source buffer
|
|
471
|
+
* Returns globalId, name, description, objectType for any IfcRoot-derived entity.
|
|
472
|
+
* This is used for entities that weren't fully parsed during initial load.
|
|
473
|
+
*/
|
|
474
|
+
export function extractEntityAttributesOnDemand(store, entityId) {
|
|
475
|
+
const ref = store.entityIndex.byId.get(entityId);
|
|
476
|
+
if (!ref) {
|
|
477
|
+
return { globalId: '', name: '', description: '', objectType: '' };
|
|
478
|
+
}
|
|
479
|
+
const extractor = new EntityExtractor(store.source);
|
|
480
|
+
const entity = extractor.extractEntity(ref);
|
|
481
|
+
if (!entity) {
|
|
482
|
+
return { globalId: '', name: '', description: '', objectType: '' };
|
|
483
|
+
}
|
|
484
|
+
const attrs = entity.attributes || [];
|
|
485
|
+
// IfcRoot attributes: [GlobalId, OwnerHistory, Name, Description]
|
|
486
|
+
// IfcObject adds: [ObjectType] at index 4
|
|
487
|
+
const globalId = typeof attrs[0] === 'string' ? attrs[0] : '';
|
|
488
|
+
const name = typeof attrs[2] === 'string' ? attrs[2] : '';
|
|
489
|
+
const description = typeof attrs[3] === 'string' ? attrs[3] : '';
|
|
490
|
+
const objectType = typeof attrs[4] === 'string' ? attrs[4] : '';
|
|
491
|
+
return { globalId, name, description, objectType };
|
|
225
492
|
}
|
|
226
493
|
//# sourceMappingURL=columnar-parser.js.map
|