@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.
Files changed (94) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +1 -1
  3. package/dist/classification-extractor.js +3 -3
  4. package/dist/classification-extractor.js.map +1 -1
  5. package/dist/columnar-parser.d.ts +86 -2
  6. package/dist/columnar-parser.d.ts.map +1 -1
  7. package/dist/columnar-parser.js +440 -173
  8. package/dist/columnar-parser.js.map +1 -1
  9. package/dist/entity-extractor.d.ts.map +1 -1
  10. package/dist/entity-extractor.js +23 -11
  11. package/dist/entity-extractor.js.map +1 -1
  12. package/dist/generated/entities.d.ts +3 -2
  13. package/dist/generated/entities.d.ts.map +1 -1
  14. package/dist/generated/entities.js +6 -3
  15. package/dist/generated/entities.js.map +1 -1
  16. package/dist/generated/enums.d.ts.map +1 -1
  17. package/dist/generated/enums.js +0 -3
  18. package/dist/generated/enums.js.map +1 -1
  19. package/dist/generated/index.d.ts +4 -2
  20. package/dist/generated/index.d.ts.map +1 -1
  21. package/dist/generated/index.js +4 -2
  22. package/dist/generated/index.js.map +1 -1
  23. package/dist/generated/schema-registry.d.ts.map +1 -1
  24. package/dist/generated/schema-registry.js +8 -3
  25. package/dist/generated/schema-registry.js.map +1 -1
  26. package/dist/generated/selects.d.ts +3 -0
  27. package/dist/generated/selects.d.ts.map +1 -1
  28. package/dist/generated/selects.js +6 -3
  29. package/dist/generated/selects.js.map +1 -1
  30. package/dist/generated/serializers.d.ts +71 -0
  31. package/dist/generated/serializers.d.ts.map +1 -0
  32. package/dist/generated/serializers.js +236 -0
  33. package/dist/generated/serializers.js.map +1 -0
  34. package/dist/generated/test-compile.d.ts +1 -6
  35. package/dist/generated/test-compile.d.ts.map +1 -1
  36. package/dist/generated/test-compile.js +30 -25
  37. package/dist/generated/test-compile.js.map +1 -1
  38. package/dist/generated/type-ids.d.ts +815 -0
  39. package/dist/generated/type-ids.d.ts.map +1 -0
  40. package/dist/generated/type-ids.js +1669 -0
  41. package/dist/generated/type-ids.js.map +1 -0
  42. package/dist/generated/types.d.ts +9 -542
  43. package/dist/generated/types.d.ts.map +1 -1
  44. package/dist/generated/types.js +6 -497
  45. package/dist/generated/types.js.map +1 -1
  46. package/dist/georef-extractor.js +2 -2
  47. package/dist/georef-extractor.js.map +1 -1
  48. package/dist/index.d.ts +38 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +233 -41
  51. package/dist/index.js.map +1 -1
  52. package/dist/material-extractor.js +8 -8
  53. package/dist/material-extractor.js.map +1 -1
  54. package/dist/parser.worker.d.ts +2 -0
  55. package/dist/parser.worker.d.ts.map +1 -0
  56. package/dist/parser.worker.js +43 -0
  57. package/dist/parser.worker.js.map +1 -0
  58. package/dist/property-extractor.d.ts +5 -1
  59. package/dist/property-extractor.d.ts.map +1 -1
  60. package/dist/property-extractor.js +22 -1
  61. package/dist/property-extractor.js.map +1 -1
  62. package/dist/quantity-extractor.d.ts +5 -1
  63. package/dist/quantity-extractor.d.ts.map +1 -1
  64. package/dist/quantity-extractor.js +29 -1
  65. package/dist/quantity-extractor.js.map +1 -1
  66. package/dist/relationship-extractor.d.ts +5 -1
  67. package/dist/relationship-extractor.d.ts.map +1 -1
  68. package/dist/relationship-extractor.js +27 -2
  69. package/dist/relationship-extractor.js.map +1 -1
  70. package/dist/spatial-hierarchy-builder.d.ts +4 -3
  71. package/dist/spatial-hierarchy-builder.d.ts.map +1 -1
  72. package/dist/spatial-hierarchy-builder.js +46 -34
  73. package/dist/spatial-hierarchy-builder.js.map +1 -1
  74. package/dist/style-extractor.d.ts +1 -0
  75. package/dist/style-extractor.d.ts.map +1 -1
  76. package/dist/style-extractor.js +18 -0
  77. package/dist/style-extractor.js.map +1 -1
  78. package/dist/tokenizer.d.ts +11 -0
  79. package/dist/tokenizer.d.ts.map +1 -1
  80. package/dist/tokenizer.js +150 -10
  81. package/dist/tokenizer.js.map +1 -1
  82. package/dist/unit-extractor.d.ts +22 -0
  83. package/dist/unit-extractor.d.ts.map +1 -0
  84. package/dist/unit-extractor.js +205 -0
  85. package/dist/unit-extractor.js.map +1 -0
  86. package/dist/worker-parser.d.ts +28 -0
  87. package/dist/worker-parser.d.ts.map +1 -0
  88. package/dist/worker-parser.js +81 -0
  89. package/dist/worker-parser.js.map +1 -0
  90. package/package.json +9 -8
  91. package/dist/examples/comprehensive-extraction.d.ts +0 -76
  92. package/dist/examples/comprehensive-extraction.d.ts.map +0 -1
  93. package/dist/examples/comprehensive-extraction.js +0 -228
  94. package/dist/examples/comprehensive-extraction.js.map +0 -1
@@ -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 { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, PropertyValueType, QuantityType, } from '@ifc-lite/data';
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 parse(buffer, entityRefs, entities, options = {}) {
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(entities.size, strings);
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
- // === Build Entity Table ===
22
- options.onProgress?.({ phase: 'entities', percent: 0 });
23
- let processed = 0;
24
- for (const [id, entity] of entities) {
25
- const attrs = entity.attributes || [];
26
- const globalId = String(attrs[0] || '');
27
- const name = String(attrs[2] || '');
28
- const description = String(attrs[3] || '');
29
- const objectType = String(attrs[7] || '');
30
- // Check if entity has geometry (simplified check)
31
- const hasGeometry = entity.type.toUpperCase().includes('WALL') ||
32
- entity.type.toUpperCase().includes('DOOR') ||
33
- entity.type.toUpperCase().includes('WINDOW') ||
34
- entity.type.toUpperCase().includes('SLAB') ||
35
- entity.type.toUpperCase().includes('COLUMN') ||
36
- entity.type.toUpperCase().includes('BEAM');
37
- const isType = entity.type.toUpperCase().endsWith('TYPE');
38
- entityTableBuilder.add(id, entity.type, globalId, name, description, objectType, hasGeometry, isType);
39
- processed++;
40
- if (processed % 1000 === 0) {
41
- options.onProgress?.({ phase: 'entities', percent: (processed / entities.size) * 100 });
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
- const entityTable = entityTableBuilder.build();
45
- options.onProgress?.({ phase: 'entities', percent: 100 });
46
- // === Build Property Table ===
47
- options.onProgress?.({ phase: 'properties', percent: 0 });
48
- const propertyExtractor = new PropertyExtractor(entities);
49
- const propertySets = propertyExtractor.extractPropertySets();
50
- // Build mapping: psetId -> entityIds
51
- const psetToEntities = new Map();
52
- const relationshipExtractor = new RelationshipExtractor(entities);
53
- const relationships = relationshipExtractor.extractRelationships();
54
- for (const rel of relationships) {
55
- if (rel.type.toUpperCase() === 'IFCRELDEFINESBYPROPERTIES') {
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
- // Extract properties into columnar format
68
- for (const [psetId, pset] of propertySets) {
69
- const entityIds = psetToEntities.get(psetId) || [];
70
- const globalId = String(entities.get(psetId)?.attributes?.[0] || '');
71
- for (const [propName, propValue] of pset.properties) {
72
- for (const entityId of entityIds) {
73
- let propType = PropertyValueType.String;
74
- let value = propValue.value;
75
- if (propValue.type === 'number') {
76
- propType = PropertyValueType.Real;
77
- value = propValue.value;
78
- }
79
- else if (propValue.type === 'boolean') {
80
- propType = PropertyValueType.Boolean;
81
- value = propValue.value;
82
- }
83
- else if (propValue.type === 'string') {
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
- const propertyTable = propertyTableBuilder.build();
99
- options.onProgress?.({ phase: 'properties', percent: 100 });
100
- // === Build Quantity Table ===
101
- options.onProgress?.({ phase: 'quantities', percent: 0 });
102
- const quantityTableBuilder = new QuantityTableBuilder(strings);
103
- const quantityExtractor = new QuantityExtractor(entities);
104
- const quantitySets = quantityExtractor.extractQuantitySets();
105
- // Build mapping: qsetId -> entityIds (similar to properties)
106
- const qsetToEntities = new Map();
107
- for (const rel of relationships) {
108
- if (rel.type.toUpperCase() === 'IFCRELDEFINESBYPROPERTIES') {
109
- const qsetId = rel.relatingObject;
110
- // Check if this is actually a quantity set (not a property set)
111
- if (quantitySets.has(qsetId)) {
112
- for (const entityId of rel.relatedObjects) {
113
- let list = qsetToEntities.get(qsetId);
114
- if (!list) {
115
- list = [];
116
- qsetToEntities.set(qsetId, list);
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
- // Map quantity type names to QuantityType enum
124
- const quantityTypeMap = {
125
- 'length': QuantityType.Length,
126
- 'area': QuantityType.Area,
127
- 'volume': QuantityType.Volume,
128
- 'count': QuantityType.Count,
129
- 'weight': QuantityType.Weight,
130
- 'time': QuantityType.Time,
131
- };
132
- // Extract quantities into columnar format
133
- for (const [qsetId, qset] of quantitySets) {
134
- const entityIds = qsetToEntities.get(qsetId) || [];
135
- for (const quantity of qset.quantities) {
136
- for (const entityId of entityIds) {
137
- quantityTableBuilder.add({
138
- entityId,
139
- qsetName: qset.name,
140
- quantityName: quantity.name,
141
- quantityType: quantityTypeMap[quantity.type] ?? QuantityType.Length,
142
- value: quantity.value,
143
- formula: quantity.formula,
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
- const quantityTable = quantityTableBuilder.build();
149
- options.onProgress?.({ phase: 'quantities', percent: 100 });
150
- // === Build Relationship Graph ===
151
- options.onProgress?.({ phase: 'relationships', percent: 0 });
152
- const relTypeMap = {
153
- 'IFCRELCONTAINEDINSPATIALSTRUCTURE': RelationshipType.ContainsElements,
154
- 'IFCRELAGGREGATES': RelationshipType.Aggregates,
155
- 'IFCRELDEFINESBYPROPERTIES': RelationshipType.DefinesByProperties,
156
- 'IFCRELDEFINESBYTYPE': RelationshipType.DefinesByType,
157
- 'IFCRELASSOCIATESMATERIAL': RelationshipType.AssociatesMaterial,
158
- 'IFCRELASSOCIATESCLASSIFICATION': RelationshipType.AssociatesClassification,
159
- 'IFCRELVOIDSELEMENT': RelationshipType.VoidsElement,
160
- 'IFCRELFILLSELEMENT': RelationshipType.FillsElement,
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
- options.onProgress?.({ phase: 'relationships', percent: 100 });
174
- // Detect schema version (simplified)
175
- let schemaVersion = 'IFC4';
176
- for (const [, entity] of entities) {
177
- if (entity.type.toUpperCase() === 'IFCPROJECT') {
178
- // Check schema version from header or entity
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
- options.onProgress?.({ phase: 'spatial-hierarchy', percent: 100 });
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: entities.size,
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