@ifc-lite/parser 1.6.1 → 1.8.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/dist/columnar-parser.d.ts +164 -5
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +882 -20
- package/dist/columnar-parser.js.map +1 -1
- package/dist/ifc-schema.d.ts +2 -1
- package/dist/ifc-schema.d.ts.map +1 -1
- package/dist/ifc-schema.js +20 -7
- package/dist/ifc-schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/columnar-parser.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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 { getAttributeNames } from './ifc-schema.js';
|
|
7
8
|
import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, } from '@ifc-lite/data';
|
|
8
9
|
// Pre-computed type sets for O(1) lookups
|
|
9
10
|
const GEOMETRY_TYPES = new Set([
|
|
@@ -21,6 +22,7 @@ const RELATIONSHIP_TYPES = new Set([
|
|
|
21
22
|
'IFCRELCONTAINEDINSPATIALSTRUCTURE', 'IFCRELAGGREGATES',
|
|
22
23
|
'IFCRELDEFINESBYPROPERTIES', 'IFCRELDEFINESBYTYPE',
|
|
23
24
|
'IFCRELASSOCIATESMATERIAL', 'IFCRELASSOCIATESCLASSIFICATION',
|
|
25
|
+
'IFCRELASSOCIATESDOCUMENT',
|
|
24
26
|
'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
|
|
25
27
|
'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
|
|
26
28
|
'IFCRELSPACEBOUNDARY',
|
|
@@ -36,6 +38,7 @@ const REL_TYPE_MAP = {
|
|
|
36
38
|
'IFCRELDEFINESBYTYPE': RelationshipType.DefinesByType,
|
|
37
39
|
'IFCRELASSOCIATESMATERIAL': RelationshipType.AssociatesMaterial,
|
|
38
40
|
'IFCRELASSOCIATESCLASSIFICATION': RelationshipType.AssociatesClassification,
|
|
41
|
+
'IFCRELASSOCIATESDOCUMENT': RelationshipType.AssociatesDocument,
|
|
39
42
|
'IFCRELVOIDSELEMENT': RelationshipType.VoidsElement,
|
|
40
43
|
'IFCRELFILLSELEMENT': RelationshipType.FillsElement,
|
|
41
44
|
'IFCRELCONNECTSPATHELEMENTS': RelationshipType.ConnectsPathElements,
|
|
@@ -60,11 +63,19 @@ const SPATIAL_TYPES = new Set([
|
|
|
60
63
|
// Relationship types needed for hierarchy
|
|
61
64
|
const HIERARCHY_REL_TYPES = new Set([
|
|
62
65
|
'IFCRELAGGREGATES', 'IFCRELCONTAINEDINSPATIALSTRUCTURE',
|
|
66
|
+
'IFCRELDEFINESBYTYPE',
|
|
63
67
|
]);
|
|
64
68
|
// Relationship types for on-demand property loading
|
|
65
69
|
const PROPERTY_REL_TYPES = new Set([
|
|
66
70
|
'IFCRELDEFINESBYPROPERTIES',
|
|
67
71
|
]);
|
|
72
|
+
// Relationship types for on-demand classification/material loading
|
|
73
|
+
const ASSOCIATION_REL_TYPES = new Set([
|
|
74
|
+
'IFCRELASSOCIATESCLASSIFICATION', 'IFCRELASSOCIATESMATERIAL',
|
|
75
|
+
'IFCRELASSOCIATESDOCUMENT',
|
|
76
|
+
]);
|
|
77
|
+
// Attributes to skip in extractAllEntityAttributes (shown elsewhere or non-displayable)
|
|
78
|
+
const SKIP_DISPLAY_ATTRS = new Set(['GlobalId', 'OwnerHistory', 'ObjectPlacement', 'Representation']);
|
|
68
79
|
// Property-related entity types for on-demand extraction
|
|
69
80
|
const PROPERTY_ENTITY_TYPES = new Set([
|
|
70
81
|
'IFCPROPERTYSET', 'IFCELEMENTQUANTITY',
|
|
@@ -74,6 +85,21 @@ const PROPERTY_ENTITY_TYPES = new Set([
|
|
|
74
85
|
'IFCQUANTITYLENGTH', 'IFCQUANTITYAREA', 'IFCQUANTITYVOLUME',
|
|
75
86
|
'IFCQUANTITYCOUNT', 'IFCQUANTITYWEIGHT', 'IFCQUANTITYTIME',
|
|
76
87
|
]);
|
|
88
|
+
/**
|
|
89
|
+
* Detect the IFC schema version from the STEP FILE_SCHEMA header.
|
|
90
|
+
* Scans the first 2000 bytes for FILE_SCHEMA(('IFC2X3')), FILE_SCHEMA(('IFC4')), etc.
|
|
91
|
+
*/
|
|
92
|
+
function detectSchemaVersion(buffer) {
|
|
93
|
+
const headerEnd = Math.min(buffer.length, 2000);
|
|
94
|
+
const headerText = new TextDecoder().decode(buffer.subarray(0, headerEnd)).toUpperCase();
|
|
95
|
+
if (headerText.includes('IFC4X3'))
|
|
96
|
+
return 'IFC4X3';
|
|
97
|
+
if (headerText.includes('IFC4'))
|
|
98
|
+
return 'IFC4';
|
|
99
|
+
if (headerText.includes('IFC2X3'))
|
|
100
|
+
return 'IFC2X3';
|
|
101
|
+
return 'IFC4'; // Default fallback
|
|
102
|
+
}
|
|
77
103
|
export class ColumnarParser {
|
|
78
104
|
/**
|
|
79
105
|
* Parse IFC file into columnar data store
|
|
@@ -87,6 +113,8 @@ export class ColumnarParser {
|
|
|
87
113
|
const uint8Buffer = new Uint8Array(buffer);
|
|
88
114
|
const totalEntities = entityRefs.length;
|
|
89
115
|
options.onProgress?.({ phase: 'building', percent: 0 });
|
|
116
|
+
// Detect schema version from FILE_SCHEMA header
|
|
117
|
+
const schemaVersion = detectSchemaVersion(uint8Buffer);
|
|
90
118
|
// Initialize builders
|
|
91
119
|
const strings = new StringTable();
|
|
92
120
|
const entityTableBuilder = new EntityTableBuilder(totalEntities, strings);
|
|
@@ -104,6 +132,7 @@ export class ColumnarParser {
|
|
|
104
132
|
const relationshipRefs = [];
|
|
105
133
|
const propertyRelRefs = [];
|
|
106
134
|
const propertyEntityRefs = [];
|
|
135
|
+
const associationRelRefs = [];
|
|
107
136
|
for (const ref of entityRefs) {
|
|
108
137
|
// Build entity index
|
|
109
138
|
entityIndex.byId.set(ref.expressId, ref);
|
|
@@ -130,6 +159,9 @@ export class ColumnarParser {
|
|
|
130
159
|
else if (PROPERTY_ENTITY_TYPES.has(typeUpper)) {
|
|
131
160
|
propertyEntityRefs.push(ref);
|
|
132
161
|
}
|
|
162
|
+
else if (ASSOCIATION_REL_TYPES.has(typeUpper)) {
|
|
163
|
+
associationRelRefs.push(ref);
|
|
164
|
+
}
|
|
133
165
|
}
|
|
134
166
|
// === TARGETED PARSING: Parse spatial and geometry entities for GlobalIds ===
|
|
135
167
|
options.onProgress?.({ phase: 'parsing spatial', percent: 10 });
|
|
@@ -220,6 +252,58 @@ export class ColumnarParser {
|
|
|
220
252
|
}
|
|
221
253
|
}
|
|
222
254
|
}
|
|
255
|
+
// === PARSE ASSOCIATION RELATIONSHIPS for on-demand classification/material/document loading ===
|
|
256
|
+
const onDemandClassificationMap = new Map();
|
|
257
|
+
const onDemandMaterialMap = new Map();
|
|
258
|
+
const onDemandDocumentMap = new Map();
|
|
259
|
+
for (const ref of associationRelRefs) {
|
|
260
|
+
const entity = extractor.extractEntity(ref);
|
|
261
|
+
if (entity) {
|
|
262
|
+
const attrs = entity.attributes || [];
|
|
263
|
+
// IfcRelAssociates subtypes:
|
|
264
|
+
// [0] GlobalId, [1] OwnerHistory, [2] Name, [3] Description
|
|
265
|
+
// [4] RelatedObjects (list of element IDs)
|
|
266
|
+
// [5] RelatingClassification / RelatingMaterial / RelatingDocument
|
|
267
|
+
const relatedObjects = attrs[4];
|
|
268
|
+
const relatingRef = attrs[5];
|
|
269
|
+
if (typeof relatingRef === 'number' && Array.isArray(relatedObjects)) {
|
|
270
|
+
const typeUpper = ref.type.toUpperCase();
|
|
271
|
+
if (typeUpper === 'IFCRELASSOCIATESCLASSIFICATION') {
|
|
272
|
+
for (const objId of relatedObjects) {
|
|
273
|
+
if (typeof objId === 'number') {
|
|
274
|
+
let list = onDemandClassificationMap.get(objId);
|
|
275
|
+
if (!list) {
|
|
276
|
+
list = [];
|
|
277
|
+
onDemandClassificationMap.set(objId, list);
|
|
278
|
+
}
|
|
279
|
+
list.push(relatingRef);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (typeUpper === 'IFCRELASSOCIATESMATERIAL') {
|
|
284
|
+
// IFC allows multiple IfcRelAssociatesMaterial per element but typically
|
|
285
|
+
// only one is valid. Last-write-wins: later relationships override earlier ones.
|
|
286
|
+
for (const objId of relatedObjects) {
|
|
287
|
+
if (typeof objId === 'number') {
|
|
288
|
+
onDemandMaterialMap.set(objId, relatingRef);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (typeUpper === 'IFCRELASSOCIATESDOCUMENT') {
|
|
293
|
+
for (const objId of relatedObjects) {
|
|
294
|
+
if (typeof objId === 'number') {
|
|
295
|
+
let list = onDemandDocumentMap.get(objId);
|
|
296
|
+
if (!list) {
|
|
297
|
+
list = [];
|
|
298
|
+
onDemandDocumentMap.set(objId, list);
|
|
299
|
+
}
|
|
300
|
+
list.push(relatingRef);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
223
307
|
// === BUILD ENTITY TABLE with spatial data included ===
|
|
224
308
|
options.onProgress?.({ phase: 'building entities', percent: 30 });
|
|
225
309
|
// OPTIMIZATION: Only add entities that are useful for the viewer UI
|
|
@@ -288,7 +372,7 @@ export class ColumnarParser {
|
|
|
288
372
|
options.onProgress?.({ phase: 'complete', percent: 100 });
|
|
289
373
|
return {
|
|
290
374
|
fileSize: buffer.byteLength,
|
|
291
|
-
schemaVersion
|
|
375
|
+
schemaVersion,
|
|
292
376
|
entityCount: totalEntities,
|
|
293
377
|
parseTime,
|
|
294
378
|
source: uint8Buffer,
|
|
@@ -301,6 +385,9 @@ export class ColumnarParser {
|
|
|
301
385
|
spatialHierarchy,
|
|
302
386
|
onDemandPropertyMap, // For instant property access
|
|
303
387
|
onDemandQuantityMap, // For instant quantity access
|
|
388
|
+
onDemandClassificationMap, // For instant classification access
|
|
389
|
+
onDemandMaterialMap, // For instant material access
|
|
390
|
+
onDemandDocumentMap, // For instant document access
|
|
304
391
|
};
|
|
305
392
|
}
|
|
306
393
|
/**
|
|
@@ -312,7 +399,7 @@ export class ColumnarParser {
|
|
|
312
399
|
return null;
|
|
313
400
|
let relatingObject;
|
|
314
401
|
let relatedObjects;
|
|
315
|
-
if (typeUpper === 'IFCRELDEFINESBYPROPERTIES' || typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE') {
|
|
402
|
+
if (typeUpper === 'IFCRELDEFINESBYPROPERTIES' || typeUpper === 'IFCRELDEFINESBYTYPE' || typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE') {
|
|
316
403
|
relatedObjects = attrs[4];
|
|
317
404
|
relatingObject = attrs[5];
|
|
318
405
|
}
|
|
@@ -371,20 +458,8 @@ export class ColumnarParser {
|
|
|
371
458
|
const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
|
|
372
459
|
if (!propName)
|
|
373
460
|
continue;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let type = 0; // String
|
|
377
|
-
let value = nominalValue;
|
|
378
|
-
if (typeof nominalValue === 'number') {
|
|
379
|
-
type = 1; // Real
|
|
380
|
-
}
|
|
381
|
-
else if (typeof nominalValue === 'boolean') {
|
|
382
|
-
type = 2; // Boolean
|
|
383
|
-
}
|
|
384
|
-
else if (nominalValue !== null && nominalValue !== undefined) {
|
|
385
|
-
value = String(nominalValue);
|
|
386
|
-
}
|
|
387
|
-
properties.push({ name: propName, type, value });
|
|
461
|
+
const parsed = parsePropertyValue(propEntity);
|
|
462
|
+
properties.push({ name: propName, type: parsed.type, value: parsed.value });
|
|
388
463
|
}
|
|
389
464
|
}
|
|
390
465
|
if (properties.length > 0 || psetName) {
|
|
@@ -467,26 +542,813 @@ export function extractQuantitiesOnDemand(store, entityId) {
|
|
|
467
542
|
}
|
|
468
543
|
/**
|
|
469
544
|
* Extract entity attributes on-demand from source buffer
|
|
470
|
-
* Returns globalId, name, description, objectType for any IfcRoot-derived entity.
|
|
545
|
+
* Returns globalId, name, description, objectType, tag for any IfcRoot-derived entity.
|
|
471
546
|
* This is used for entities that weren't fully parsed during initial load.
|
|
472
547
|
*/
|
|
473
548
|
export function extractEntityAttributesOnDemand(store, entityId) {
|
|
474
549
|
const ref = store.entityIndex.byId.get(entityId);
|
|
475
550
|
if (!ref) {
|
|
476
|
-
return { globalId: '', name: '', description: '', objectType: '' };
|
|
551
|
+
return { globalId: '', name: '', description: '', objectType: '', tag: '' };
|
|
477
552
|
}
|
|
478
553
|
const extractor = new EntityExtractor(store.source);
|
|
479
554
|
const entity = extractor.extractEntity(ref);
|
|
480
555
|
if (!entity) {
|
|
481
|
-
return { globalId: '', name: '', description: '', objectType: '' };
|
|
556
|
+
return { globalId: '', name: '', description: '', objectType: '', tag: '' };
|
|
482
557
|
}
|
|
483
558
|
const attrs = entity.attributes || [];
|
|
484
559
|
// IfcRoot attributes: [GlobalId, OwnerHistory, Name, Description]
|
|
485
560
|
// IfcObject adds: [ObjectType] at index 4
|
|
561
|
+
// IfcProduct adds: [ObjectPlacement, Representation] at indices 5-6
|
|
562
|
+
// IfcElement adds: [Tag] at index 7
|
|
486
563
|
const globalId = typeof attrs[0] === 'string' ? attrs[0] : '';
|
|
487
564
|
const name = typeof attrs[2] === 'string' ? attrs[2] : '';
|
|
488
565
|
const description = typeof attrs[3] === 'string' ? attrs[3] : '';
|
|
489
566
|
const objectType = typeof attrs[4] === 'string' ? attrs[4] : '';
|
|
490
|
-
|
|
567
|
+
const tag = typeof attrs[7] === 'string' ? attrs[7] : '';
|
|
568
|
+
return { globalId, name, description, objectType, tag };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Extract ALL named entity attributes on-demand from source buffer.
|
|
572
|
+
* Uses the IFC schema to map attribute indices to names.
|
|
573
|
+
* Returns only string/enum attributes, skipping references and structural attributes.
|
|
574
|
+
*/
|
|
575
|
+
export function extractAllEntityAttributes(store, entityId) {
|
|
576
|
+
const ref = store.entityIndex.byId.get(entityId);
|
|
577
|
+
if (!ref)
|
|
578
|
+
return [];
|
|
579
|
+
const extractor = new EntityExtractor(store.source);
|
|
580
|
+
const entity = extractor.extractEntity(ref);
|
|
581
|
+
if (!entity)
|
|
582
|
+
return [];
|
|
583
|
+
const attrs = entity.attributes || [];
|
|
584
|
+
// Use properly-cased type name from entity table (IfcTypeEnumToString)
|
|
585
|
+
// instead of ref.type which is UPPERCASE from STEP (e.g., IFCWALLSTANDARDCASE)
|
|
586
|
+
// and breaks multi-word type normalization in getAttributeNames
|
|
587
|
+
const typeName = store.entities.getTypeName(entityId);
|
|
588
|
+
const attrNames = getAttributeNames(typeName || ref.type);
|
|
589
|
+
const result = [];
|
|
590
|
+
const len = Math.min(attrs.length, attrNames.length);
|
|
591
|
+
for (let i = 0; i < len; i++) {
|
|
592
|
+
const attrName = attrNames[i];
|
|
593
|
+
if (SKIP_DISPLAY_ATTRS.has(attrName))
|
|
594
|
+
continue;
|
|
595
|
+
const raw = attrs[i];
|
|
596
|
+
if (typeof raw === 'string' && raw) {
|
|
597
|
+
// Clean enum values: .NOTDEFINED. -> NOTDEFINED
|
|
598
|
+
const display = raw.startsWith('.') && raw.endsWith('.')
|
|
599
|
+
? raw.slice(1, -1)
|
|
600
|
+
: raw;
|
|
601
|
+
result.push({ name: attrName, value: display });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Extract classifications for a single entity ON-DEMAND.
|
|
608
|
+
* Uses the onDemandClassificationMap built during parsing.
|
|
609
|
+
* Falls back to relationship graph when on-demand map is not available (e.g., server-loaded models).
|
|
610
|
+
* Also checks type-level associations via IfcRelDefinesByType.
|
|
611
|
+
* Returns an array of classification references with system info.
|
|
612
|
+
*/
|
|
613
|
+
export function extractClassificationsOnDemand(store, entityId) {
|
|
614
|
+
let classRefIds;
|
|
615
|
+
if (store.onDemandClassificationMap) {
|
|
616
|
+
classRefIds = store.onDemandClassificationMap.get(entityId);
|
|
617
|
+
}
|
|
618
|
+
else if (store.relationships) {
|
|
619
|
+
// Fallback: use relationship graph (server-loaded models)
|
|
620
|
+
const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesClassification, 'inverse');
|
|
621
|
+
if (related.length > 0)
|
|
622
|
+
classRefIds = related;
|
|
623
|
+
}
|
|
624
|
+
// Also check type-level classifications via IfcRelDefinesByType
|
|
625
|
+
if (store.relationships) {
|
|
626
|
+
const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
|
|
627
|
+
for (const typeId of typeIds) {
|
|
628
|
+
let typeClassRefs;
|
|
629
|
+
if (store.onDemandClassificationMap) {
|
|
630
|
+
typeClassRefs = store.onDemandClassificationMap.get(typeId);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesClassification, 'inverse');
|
|
634
|
+
if (related.length > 0)
|
|
635
|
+
typeClassRefs = related;
|
|
636
|
+
}
|
|
637
|
+
if (typeClassRefs && typeClassRefs.length > 0) {
|
|
638
|
+
classRefIds = classRefIds ? [...classRefIds, ...typeClassRefs] : [...typeClassRefs];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (!classRefIds || classRefIds.length === 0)
|
|
643
|
+
return [];
|
|
644
|
+
if (!store.source?.length)
|
|
645
|
+
return [];
|
|
646
|
+
const extractor = new EntityExtractor(store.source);
|
|
647
|
+
const results = [];
|
|
648
|
+
for (const classRefId of classRefIds) {
|
|
649
|
+
const ref = store.entityIndex.byId.get(classRefId);
|
|
650
|
+
if (!ref)
|
|
651
|
+
continue;
|
|
652
|
+
const entity = extractor.extractEntity(ref);
|
|
653
|
+
if (!entity)
|
|
654
|
+
continue;
|
|
655
|
+
const typeUpper = entity.type.toUpperCase();
|
|
656
|
+
const attrs = entity.attributes || [];
|
|
657
|
+
if (typeUpper === 'IFCCLASSIFICATIONREFERENCE') {
|
|
658
|
+
// IfcClassificationReference: [Location, Identification, Name, ReferencedSource, Description, Sort]
|
|
659
|
+
const info = {
|
|
660
|
+
location: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
661
|
+
identification: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
662
|
+
name: typeof attrs[2] === 'string' ? attrs[2] : undefined,
|
|
663
|
+
description: typeof attrs[4] === 'string' ? attrs[4] : undefined,
|
|
664
|
+
};
|
|
665
|
+
// Walk up to find the classification system name
|
|
666
|
+
const referencedSourceId = typeof attrs[3] === 'number' ? attrs[3] : undefined;
|
|
667
|
+
if (referencedSourceId) {
|
|
668
|
+
const path = walkClassificationChain(store, extractor, referencedSourceId);
|
|
669
|
+
info.system = path.systemName;
|
|
670
|
+
info.path = path.codes;
|
|
671
|
+
}
|
|
672
|
+
results.push(info);
|
|
673
|
+
}
|
|
674
|
+
else if (typeUpper === 'IFCCLASSIFICATION') {
|
|
675
|
+
// IfcClassification: [Source, Edition, EditionDate, Name, Description, Location, ReferenceTokens]
|
|
676
|
+
results.push({
|
|
677
|
+
system: typeof attrs[3] === 'string' ? attrs[3] : undefined,
|
|
678
|
+
name: typeof attrs[3] === 'string' ? attrs[3] : undefined,
|
|
679
|
+
description: typeof attrs[4] === 'string' ? attrs[4] : undefined,
|
|
680
|
+
location: typeof attrs[5] === 'string' ? attrs[5] : undefined,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return results;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Walk up the IfcClassificationReference chain to find the root IfcClassification system.
|
|
688
|
+
*/
|
|
689
|
+
function walkClassificationChain(store, extractor, startId) {
|
|
690
|
+
const codes = [];
|
|
691
|
+
let currentId = startId;
|
|
692
|
+
const visited = new Set();
|
|
693
|
+
while (currentId !== undefined && !visited.has(currentId)) {
|
|
694
|
+
visited.add(currentId);
|
|
695
|
+
const ref = store.entityIndex.byId.get(currentId);
|
|
696
|
+
if (!ref)
|
|
697
|
+
break;
|
|
698
|
+
const entity = extractor.extractEntity(ref);
|
|
699
|
+
if (!entity)
|
|
700
|
+
break;
|
|
701
|
+
const typeUpper = entity.type.toUpperCase();
|
|
702
|
+
const attrs = entity.attributes || [];
|
|
703
|
+
if (typeUpper === 'IFCCLASSIFICATION') {
|
|
704
|
+
// Root: IfcClassification [Source, Edition, EditionDate, Name, ...]
|
|
705
|
+
const systemName = typeof attrs[3] === 'string' ? attrs[3] : undefined;
|
|
706
|
+
return { systemName, codes };
|
|
707
|
+
}
|
|
708
|
+
if (typeUpper === 'IFCCLASSIFICATIONREFERENCE') {
|
|
709
|
+
// IfcClassificationReference [Location, Identification, Name, ReferencedSource, ...]
|
|
710
|
+
const code = typeof attrs[1] === 'string' ? attrs[1] :
|
|
711
|
+
typeof attrs[2] === 'string' ? attrs[2] : undefined;
|
|
712
|
+
if (code)
|
|
713
|
+
codes.unshift(code);
|
|
714
|
+
currentId = typeof attrs[3] === 'number' ? attrs[3] : undefined;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { codes };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Extract materials for a single entity ON-DEMAND.
|
|
724
|
+
* Uses the onDemandMaterialMap built during parsing.
|
|
725
|
+
* Falls back to relationship graph when on-demand map is not available (e.g., server-loaded models).
|
|
726
|
+
* Also checks type-level material assignments via IfcRelDefinesByType.
|
|
727
|
+
* Resolves the full material structure (layers, profiles, constituents, lists).
|
|
728
|
+
*/
|
|
729
|
+
export function extractMaterialsOnDemand(store, entityId) {
|
|
730
|
+
let materialId;
|
|
731
|
+
if (store.onDemandMaterialMap) {
|
|
732
|
+
materialId = store.onDemandMaterialMap.get(entityId);
|
|
733
|
+
}
|
|
734
|
+
else if (store.relationships) {
|
|
735
|
+
// Fallback: use relationship graph (server-loaded models)
|
|
736
|
+
const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesMaterial, 'inverse');
|
|
737
|
+
if (related.length > 0)
|
|
738
|
+
materialId = related[0];
|
|
739
|
+
}
|
|
740
|
+
// Check type-level material if occurrence has none
|
|
741
|
+
if (materialId === undefined && store.relationships) {
|
|
742
|
+
const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
|
|
743
|
+
for (const typeId of typeIds) {
|
|
744
|
+
if (store.onDemandMaterialMap) {
|
|
745
|
+
materialId = store.onDemandMaterialMap.get(typeId);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesMaterial, 'inverse');
|
|
749
|
+
if (related.length > 0)
|
|
750
|
+
materialId = related[0];
|
|
751
|
+
}
|
|
752
|
+
if (materialId !== undefined)
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (materialId === undefined)
|
|
757
|
+
return null;
|
|
758
|
+
if (!store.source?.length)
|
|
759
|
+
return null;
|
|
760
|
+
const extractor = new EntityExtractor(store.source);
|
|
761
|
+
return resolveMaterial(store, extractor, materialId, new Set());
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Resolve a material entity by ID, handling all IFC material types.
|
|
765
|
+
* Uses visited set to prevent infinite recursion on cyclic *Usage references.
|
|
766
|
+
*/
|
|
767
|
+
function resolveMaterial(store, extractor, materialId, visited = new Set()) {
|
|
768
|
+
if (visited.has(materialId))
|
|
769
|
+
return null;
|
|
770
|
+
visited.add(materialId);
|
|
771
|
+
const ref = store.entityIndex.byId.get(materialId);
|
|
772
|
+
if (!ref)
|
|
773
|
+
return null;
|
|
774
|
+
const entity = extractor.extractEntity(ref);
|
|
775
|
+
if (!entity)
|
|
776
|
+
return null;
|
|
777
|
+
const typeUpper = entity.type.toUpperCase();
|
|
778
|
+
const attrs = entity.attributes || [];
|
|
779
|
+
switch (typeUpper) {
|
|
780
|
+
case 'IFCMATERIAL': {
|
|
781
|
+
// IfcMaterial: [Name, Description, Category]
|
|
782
|
+
return {
|
|
783
|
+
type: 'Material',
|
|
784
|
+
name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
785
|
+
description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
case 'IFCMATERIALLAYERSET': {
|
|
789
|
+
// IfcMaterialLayerSet: [MaterialLayers, LayerSetName, Description]
|
|
790
|
+
const layerIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
|
|
791
|
+
const layers = [];
|
|
792
|
+
for (const layerId of layerIds) {
|
|
793
|
+
const layerRef = store.entityIndex.byId.get(layerId);
|
|
794
|
+
if (!layerRef)
|
|
795
|
+
continue;
|
|
796
|
+
const layerEntity = extractor.extractEntity(layerRef);
|
|
797
|
+
if (!layerEntity)
|
|
798
|
+
continue;
|
|
799
|
+
const la = layerEntity.attributes || [];
|
|
800
|
+
// IfcMaterialLayer: [Material, LayerThickness, IsVentilated, Name, Description, Category, Priority]
|
|
801
|
+
const matId = typeof la[0] === 'number' ? la[0] : undefined;
|
|
802
|
+
let materialName;
|
|
803
|
+
if (matId) {
|
|
804
|
+
const matRef = store.entityIndex.byId.get(matId);
|
|
805
|
+
if (matRef) {
|
|
806
|
+
const matEntity = extractor.extractEntity(matRef);
|
|
807
|
+
if (matEntity) {
|
|
808
|
+
materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
layers.push({
|
|
813
|
+
materialName,
|
|
814
|
+
thickness: typeof la[1] === 'number' ? la[1] : undefined,
|
|
815
|
+
isVentilated: la[2] === true || la[2] === '.T.',
|
|
816
|
+
name: typeof la[3] === 'string' ? la[3] : undefined,
|
|
817
|
+
category: typeof la[5] === 'string' ? la[5] : undefined,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
type: 'MaterialLayerSet',
|
|
822
|
+
name: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
823
|
+
description: typeof attrs[2] === 'string' ? attrs[2] : undefined,
|
|
824
|
+
layers,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
case 'IFCMATERIALPROFILESET': {
|
|
828
|
+
// IfcMaterialProfileSet: [Name, Description, MaterialProfiles, CompositeProfile]
|
|
829
|
+
const profileIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
|
|
830
|
+
const profiles = [];
|
|
831
|
+
for (const profId of profileIds) {
|
|
832
|
+
const profRef = store.entityIndex.byId.get(profId);
|
|
833
|
+
if (!profRef)
|
|
834
|
+
continue;
|
|
835
|
+
const profEntity = extractor.extractEntity(profRef);
|
|
836
|
+
if (!profEntity)
|
|
837
|
+
continue;
|
|
838
|
+
const pa = profEntity.attributes || [];
|
|
839
|
+
// IfcMaterialProfile: [Name, Description, Material, Profile, Priority, Category]
|
|
840
|
+
const matId = typeof pa[2] === 'number' ? pa[2] : undefined;
|
|
841
|
+
let materialName;
|
|
842
|
+
if (matId) {
|
|
843
|
+
const matRef = store.entityIndex.byId.get(matId);
|
|
844
|
+
if (matRef) {
|
|
845
|
+
const matEntity = extractor.extractEntity(matRef);
|
|
846
|
+
if (matEntity) {
|
|
847
|
+
materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
profiles.push({
|
|
852
|
+
materialName,
|
|
853
|
+
name: typeof pa[0] === 'string' ? pa[0] : undefined,
|
|
854
|
+
category: typeof pa[5] === 'string' ? pa[5] : undefined,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
type: 'MaterialProfileSet',
|
|
859
|
+
name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
860
|
+
description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
861
|
+
profiles,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
case 'IFCMATERIALCONSTITUENTSET': {
|
|
865
|
+
// IfcMaterialConstituentSet: [Name, Description, MaterialConstituents]
|
|
866
|
+
const constituentIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
|
|
867
|
+
const constituents = [];
|
|
868
|
+
for (const constId of constituentIds) {
|
|
869
|
+
const constRef = store.entityIndex.byId.get(constId);
|
|
870
|
+
if (!constRef)
|
|
871
|
+
continue;
|
|
872
|
+
const constEntity = extractor.extractEntity(constRef);
|
|
873
|
+
if (!constEntity)
|
|
874
|
+
continue;
|
|
875
|
+
const ca = constEntity.attributes || [];
|
|
876
|
+
// IfcMaterialConstituent: [Name, Description, Material, Fraction, Category]
|
|
877
|
+
const matId = typeof ca[2] === 'number' ? ca[2] : undefined;
|
|
878
|
+
let materialName;
|
|
879
|
+
if (matId) {
|
|
880
|
+
const matRef = store.entityIndex.byId.get(matId);
|
|
881
|
+
if (matRef) {
|
|
882
|
+
const matEntity = extractor.extractEntity(matRef);
|
|
883
|
+
if (matEntity) {
|
|
884
|
+
materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
constituents.push({
|
|
889
|
+
materialName,
|
|
890
|
+
name: typeof ca[0] === 'string' ? ca[0] : undefined,
|
|
891
|
+
fraction: typeof ca[3] === 'number' ? ca[3] : undefined,
|
|
892
|
+
category: typeof ca[4] === 'string' ? ca[4] : undefined,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
type: 'MaterialConstituentSet',
|
|
897
|
+
name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
898
|
+
description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
899
|
+
constituents,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
case 'IFCMATERIALLIST': {
|
|
903
|
+
// IfcMaterialList: [Materials]
|
|
904
|
+
const matIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
|
|
905
|
+
const materials = [];
|
|
906
|
+
for (const matId of matIds) {
|
|
907
|
+
const matRef = store.entityIndex.byId.get(matId);
|
|
908
|
+
if (!matRef)
|
|
909
|
+
continue;
|
|
910
|
+
const matEntity = extractor.extractEntity(matRef);
|
|
911
|
+
if (matEntity) {
|
|
912
|
+
const name = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : `Material #${matId}`;
|
|
913
|
+
materials.push(name);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
type: 'MaterialList',
|
|
918
|
+
materials,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
case 'IFCMATERIALLAYERSETUSAGE': {
|
|
922
|
+
// IfcMaterialLayerSetUsage: [ForLayerSet, LayerSetDirection, DirectionSense, OffsetFromReferenceLine, ...]
|
|
923
|
+
const layerSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
|
|
924
|
+
if (layerSetId) {
|
|
925
|
+
return resolveMaterial(store, extractor, layerSetId, visited);
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
case 'IFCMATERIALPROFILESETUSAGE': {
|
|
930
|
+
// IfcMaterialProfileSetUsage: [ForProfileSet, ...]
|
|
931
|
+
const profileSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
|
|
932
|
+
if (profileSetId) {
|
|
933
|
+
return resolveMaterial(store, extractor, profileSetId, visited);
|
|
934
|
+
}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
default:
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Parse a property entity's value based on its IFC type.
|
|
943
|
+
* Handles all 6 IfcProperty subtypes:
|
|
944
|
+
* - IfcPropertySingleValue: direct value
|
|
945
|
+
* - IfcPropertyEnumeratedValue: list of enum values → joined string
|
|
946
|
+
* - IfcPropertyBoundedValue: upper/lower bounds → "value [min – max]"
|
|
947
|
+
* - IfcPropertyListValue: list of values → joined string
|
|
948
|
+
* - IfcPropertyTableValue: defining/defined value pairs → "Table(N rows)"
|
|
949
|
+
* - IfcPropertyReferenceValue: entity reference → "Reference #ID"
|
|
950
|
+
*/
|
|
951
|
+
function parsePropertyValue(propEntity) {
|
|
952
|
+
const attrs = propEntity.attributes || [];
|
|
953
|
+
const typeUpper = propEntity.type.toUpperCase();
|
|
954
|
+
switch (typeUpper) {
|
|
955
|
+
case 'IFCPROPERTYENUMERATEDVALUE': {
|
|
956
|
+
// [Name, Description, EnumerationValues (list), EnumerationReference]
|
|
957
|
+
const enumValues = attrs[2];
|
|
958
|
+
if (Array.isArray(enumValues)) {
|
|
959
|
+
const values = enumValues.map(v => {
|
|
960
|
+
if (Array.isArray(v) && v.length === 2)
|
|
961
|
+
return String(v[1]); // Typed value
|
|
962
|
+
return String(v);
|
|
963
|
+
}).filter(v => v !== 'null' && v !== 'undefined');
|
|
964
|
+
return { type: 0, value: values.join(', ') };
|
|
965
|
+
}
|
|
966
|
+
return { type: 0, value: null };
|
|
967
|
+
}
|
|
968
|
+
case 'IFCPROPERTYBOUNDEDVALUE': {
|
|
969
|
+
// [Name, Description, UpperBoundValue, LowerBoundValue, Unit, SetPointValue]
|
|
970
|
+
const upper = extractNumericValue(attrs[2]);
|
|
971
|
+
const lower = extractNumericValue(attrs[3]);
|
|
972
|
+
const setPoint = extractNumericValue(attrs[5]);
|
|
973
|
+
const displayValue = setPoint ?? upper ?? lower;
|
|
974
|
+
let display = displayValue != null ? String(displayValue) : '';
|
|
975
|
+
if (lower != null && upper != null) {
|
|
976
|
+
display += ` [${lower} – ${upper}]`;
|
|
977
|
+
}
|
|
978
|
+
return { type: displayValue != null ? 1 : 0, value: display || null };
|
|
979
|
+
}
|
|
980
|
+
case 'IFCPROPERTYLISTVALUE': {
|
|
981
|
+
// [Name, Description, ListValues (list), Unit]
|
|
982
|
+
const listValues = attrs[2];
|
|
983
|
+
if (Array.isArray(listValues)) {
|
|
984
|
+
const values = listValues.map(v => {
|
|
985
|
+
if (Array.isArray(v) && v.length === 2)
|
|
986
|
+
return String(v[1]);
|
|
987
|
+
return String(v);
|
|
988
|
+
}).filter(v => v !== 'null' && v !== 'undefined');
|
|
989
|
+
return { type: 0, value: values.join(', ') };
|
|
990
|
+
}
|
|
991
|
+
return { type: 0, value: null };
|
|
992
|
+
}
|
|
993
|
+
case 'IFCPROPERTYTABLEVALUE': {
|
|
994
|
+
// [Name, Description, DefiningValues, DefinedValues, ...]
|
|
995
|
+
const definingValues = attrs[2];
|
|
996
|
+
const definedValues = attrs[3];
|
|
997
|
+
const rowCount = Array.isArray(definingValues) ? definingValues.length : 0;
|
|
998
|
+
if (rowCount > 0 && Array.isArray(definedValues)) {
|
|
999
|
+
return { type: 0, value: `Table (${rowCount} rows)` };
|
|
1000
|
+
}
|
|
1001
|
+
return { type: 0, value: null };
|
|
1002
|
+
}
|
|
1003
|
+
case 'IFCPROPERTYREFERENCEVALUE': {
|
|
1004
|
+
// [Name, Description, PropertyReference]
|
|
1005
|
+
const refValue = attrs[2];
|
|
1006
|
+
if (typeof refValue === 'number') {
|
|
1007
|
+
return { type: 0, value: `#${refValue}` };
|
|
1008
|
+
}
|
|
1009
|
+
return { type: 0, value: null };
|
|
1010
|
+
}
|
|
1011
|
+
default: {
|
|
1012
|
+
// IfcPropertySingleValue and fallback: [Name, Description, NominalValue, Unit]
|
|
1013
|
+
const nominalValue = attrs[2];
|
|
1014
|
+
let type = 0;
|
|
1015
|
+
let value = nominalValue;
|
|
1016
|
+
// Handle typed values like IFCBOOLEAN(.T.), IFCREAL(1.5)
|
|
1017
|
+
if (Array.isArray(nominalValue) && nominalValue.length === 2) {
|
|
1018
|
+
const innerValue = nominalValue[1];
|
|
1019
|
+
const typeName = String(nominalValue[0]).toUpperCase();
|
|
1020
|
+
if (typeName.includes('BOOLEAN') || typeName.includes('LOGICAL')) {
|
|
1021
|
+
type = 2;
|
|
1022
|
+
value = innerValue === '.T.' || innerValue === true;
|
|
1023
|
+
}
|
|
1024
|
+
else if (typeof innerValue === 'number') {
|
|
1025
|
+
type = 1;
|
|
1026
|
+
value = innerValue;
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
type = 0;
|
|
1030
|
+
value = String(innerValue);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
else if (typeof nominalValue === 'number') {
|
|
1034
|
+
type = 1;
|
|
1035
|
+
}
|
|
1036
|
+
else if (typeof nominalValue === 'boolean') {
|
|
1037
|
+
type = 2;
|
|
1038
|
+
}
|
|
1039
|
+
else if (nominalValue !== null && nominalValue !== undefined) {
|
|
1040
|
+
value = String(nominalValue);
|
|
1041
|
+
}
|
|
1042
|
+
return { type, value };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/** Extract a numeric value from a possibly typed STEP value. */
|
|
1047
|
+
function extractNumericValue(attr) {
|
|
1048
|
+
if (typeof attr === 'number')
|
|
1049
|
+
return attr;
|
|
1050
|
+
if (Array.isArray(attr) && attr.length === 2 && typeof attr[1] === 'number')
|
|
1051
|
+
return attr[1];
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Extract property sets from a list of pset IDs using the entity index.
|
|
1056
|
+
* Shared logic between instance-level and type-level property extraction.
|
|
1057
|
+
*/
|
|
1058
|
+
function extractPsetsFromIds(store, extractor, psetIds) {
|
|
1059
|
+
const result = [];
|
|
1060
|
+
for (const psetId of psetIds) {
|
|
1061
|
+
const psetRef = store.entityIndex.byId.get(psetId);
|
|
1062
|
+
if (!psetRef)
|
|
1063
|
+
continue;
|
|
1064
|
+
// Only extract IFCPROPERTYSET entities (skip quantity sets etc.)
|
|
1065
|
+
if (psetRef.type.toUpperCase() !== 'IFCPROPERTYSET')
|
|
1066
|
+
continue;
|
|
1067
|
+
const psetEntity = extractor.extractEntity(psetRef);
|
|
1068
|
+
if (!psetEntity)
|
|
1069
|
+
continue;
|
|
1070
|
+
const psetAttrs = psetEntity.attributes || [];
|
|
1071
|
+
const psetGlobalId = typeof psetAttrs[0] === 'string' ? psetAttrs[0] : undefined;
|
|
1072
|
+
const psetName = typeof psetAttrs[2] === 'string' ? psetAttrs[2] : `PropertySet #${psetId}`;
|
|
1073
|
+
const hasProperties = psetAttrs[4];
|
|
1074
|
+
const properties = [];
|
|
1075
|
+
if (Array.isArray(hasProperties)) {
|
|
1076
|
+
for (const propRef of hasProperties) {
|
|
1077
|
+
if (typeof propRef !== 'number')
|
|
1078
|
+
continue;
|
|
1079
|
+
const propEntityRef = store.entityIndex.byId.get(propRef);
|
|
1080
|
+
if (!propEntityRef)
|
|
1081
|
+
continue;
|
|
1082
|
+
const propEntity = extractor.extractEntity(propEntityRef);
|
|
1083
|
+
if (!propEntity)
|
|
1084
|
+
continue;
|
|
1085
|
+
const propAttrs = propEntity.attributes || [];
|
|
1086
|
+
const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
|
|
1087
|
+
if (!propName)
|
|
1088
|
+
continue;
|
|
1089
|
+
const parsed = parsePropertyValue(propEntity);
|
|
1090
|
+
properties.push({ name: propName, type: parsed.type, value: parsed.value });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (properties.length > 0 || psetName) {
|
|
1094
|
+
result.push({ name: psetName, globalId: psetGlobalId, properties });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Extract type-level properties for a single entity ON-DEMAND.
|
|
1101
|
+
* Finds the element's type via IfcRelDefinesByType, then extracts property sets from:
|
|
1102
|
+
* 1. The type entity's HasPropertySets attribute (IFC2X3/IFC4: index 5 on IfcTypeObject)
|
|
1103
|
+
* 2. The onDemandPropertyMap for the type entity (IFC4 IFCRELDEFINESBYPROPERTIES → type)
|
|
1104
|
+
* Returns null if no type relationship exists.
|
|
1105
|
+
*/
|
|
1106
|
+
export function extractTypePropertiesOnDemand(store, entityId) {
|
|
1107
|
+
if (!store.relationships)
|
|
1108
|
+
return null;
|
|
1109
|
+
// Find type entity via DefinesByType relationship (inverse: element → type)
|
|
1110
|
+
const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
|
|
1111
|
+
if (typeIds.length === 0)
|
|
1112
|
+
return null;
|
|
1113
|
+
const typeId = typeIds[0]; // An element typically has one type
|
|
1114
|
+
const typeRef = store.entityIndex.byId.get(typeId);
|
|
1115
|
+
if (!typeRef)
|
|
1116
|
+
return null;
|
|
1117
|
+
if (!store.source?.length)
|
|
1118
|
+
return null;
|
|
1119
|
+
const extractor = new EntityExtractor(store.source);
|
|
1120
|
+
// Get type name from entity
|
|
1121
|
+
const typeEntity = extractor.extractEntity(typeRef);
|
|
1122
|
+
const typeName = typeEntity && typeof typeEntity.attributes?.[2] === 'string'
|
|
1123
|
+
? typeEntity.attributes[2]
|
|
1124
|
+
: typeRef.type;
|
|
1125
|
+
const allPsets = [];
|
|
1126
|
+
const seenPsetNames = new Set();
|
|
1127
|
+
// Source 1: HasPropertySets attribute on the type entity (index 5 for IfcTypeObject subtypes)
|
|
1128
|
+
// Works for both IFC2X3 and IFC4
|
|
1129
|
+
if (typeEntity) {
|
|
1130
|
+
const hasPropertySets = typeEntity.attributes?.[5];
|
|
1131
|
+
if (Array.isArray(hasPropertySets)) {
|
|
1132
|
+
const psetIds = hasPropertySets.filter((id) => typeof id === 'number');
|
|
1133
|
+
const psets = extractPsetsFromIds(store, extractor, psetIds);
|
|
1134
|
+
for (const pset of psets) {
|
|
1135
|
+
seenPsetNames.add(pset.name);
|
|
1136
|
+
allPsets.push(pset);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// Source 2: onDemandPropertyMap for the type entity (IFC4: via IFCRELDEFINESBYPROPERTIES)
|
|
1141
|
+
if (store.onDemandPropertyMap) {
|
|
1142
|
+
const typePsetIds = store.onDemandPropertyMap.get(typeId);
|
|
1143
|
+
if (typePsetIds && typePsetIds.length > 0) {
|
|
1144
|
+
const psets = extractPsetsFromIds(store, extractor, typePsetIds);
|
|
1145
|
+
for (const pset of psets) {
|
|
1146
|
+
if (!seenPsetNames.has(pset.name)) {
|
|
1147
|
+
allPsets.push(pset);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (allPsets.length === 0)
|
|
1153
|
+
return null;
|
|
1154
|
+
return {
|
|
1155
|
+
typeName,
|
|
1156
|
+
typeId,
|
|
1157
|
+
properties: allPsets,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Extract documents for a single entity ON-DEMAND.
|
|
1162
|
+
* Uses the onDemandDocumentMap built during parsing.
|
|
1163
|
+
* Falls back to relationship graph when on-demand map is not available.
|
|
1164
|
+
* Also checks type-level documents via IfcRelDefinesByType.
|
|
1165
|
+
* Returns an array of document info objects.
|
|
1166
|
+
*/
|
|
1167
|
+
export function extractDocumentsOnDemand(store, entityId) {
|
|
1168
|
+
let docRefIds;
|
|
1169
|
+
if (store.onDemandDocumentMap) {
|
|
1170
|
+
docRefIds = store.onDemandDocumentMap.get(entityId);
|
|
1171
|
+
}
|
|
1172
|
+
else if (store.relationships) {
|
|
1173
|
+
const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesDocument, 'inverse');
|
|
1174
|
+
if (related.length > 0)
|
|
1175
|
+
docRefIds = related;
|
|
1176
|
+
}
|
|
1177
|
+
// Also check type-level documents via IfcRelDefinesByType
|
|
1178
|
+
if (store.relationships) {
|
|
1179
|
+
const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
|
|
1180
|
+
for (const typeId of typeIds) {
|
|
1181
|
+
let typeDocRefs;
|
|
1182
|
+
if (store.onDemandDocumentMap) {
|
|
1183
|
+
typeDocRefs = store.onDemandDocumentMap.get(typeId);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesDocument, 'inverse');
|
|
1187
|
+
if (related.length > 0)
|
|
1188
|
+
typeDocRefs = related;
|
|
1189
|
+
}
|
|
1190
|
+
if (typeDocRefs && typeDocRefs.length > 0) {
|
|
1191
|
+
docRefIds = docRefIds ? [...docRefIds, ...typeDocRefs] : [...typeDocRefs];
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!docRefIds || docRefIds.length === 0)
|
|
1196
|
+
return [];
|
|
1197
|
+
if (!store.source?.length)
|
|
1198
|
+
return [];
|
|
1199
|
+
const extractor = new EntityExtractor(store.source);
|
|
1200
|
+
const results = [];
|
|
1201
|
+
for (const docId of docRefIds) {
|
|
1202
|
+
const docRef = store.entityIndex.byId.get(docId);
|
|
1203
|
+
if (!docRef)
|
|
1204
|
+
continue;
|
|
1205
|
+
const docEntity = extractor.extractEntity(docRef);
|
|
1206
|
+
if (!docEntity)
|
|
1207
|
+
continue;
|
|
1208
|
+
const typeUpper = docEntity.type.toUpperCase();
|
|
1209
|
+
const attrs = docEntity.attributes || [];
|
|
1210
|
+
if (typeUpper === 'IFCDOCUMENTREFERENCE') {
|
|
1211
|
+
// IFC4: [Location, Identification, Name, Description, ReferencedDocument]
|
|
1212
|
+
// IFC2X3: [Location, ItemReference, Name]
|
|
1213
|
+
const info = {
|
|
1214
|
+
location: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
1215
|
+
identification: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
1216
|
+
name: typeof attrs[2] === 'string' ? attrs[2] : undefined,
|
|
1217
|
+
description: typeof attrs[3] === 'string' ? attrs[3] : undefined,
|
|
1218
|
+
};
|
|
1219
|
+
// Walk to IfcDocumentInformation if ReferencedDocument is set (IFC4 attr[4])
|
|
1220
|
+
if (typeof attrs[4] === 'number') {
|
|
1221
|
+
const docInfoRef = store.entityIndex.byId.get(attrs[4]);
|
|
1222
|
+
if (docInfoRef) {
|
|
1223
|
+
const docInfoEntity = extractor.extractEntity(docInfoRef);
|
|
1224
|
+
if (docInfoEntity && docInfoEntity.type.toUpperCase() === 'IFCDOCUMENTINFORMATION') {
|
|
1225
|
+
const ia = docInfoEntity.attributes || [];
|
|
1226
|
+
// IfcDocumentInformation: [Identification, Name, Description, Location, Purpose, IntendedUse, Scope, Revision, ...]
|
|
1227
|
+
if (!info.identification && typeof ia[0] === 'string')
|
|
1228
|
+
info.identification = ia[0];
|
|
1229
|
+
if (!info.name && typeof ia[1] === 'string')
|
|
1230
|
+
info.name = ia[1];
|
|
1231
|
+
if (!info.description && typeof ia[2] === 'string')
|
|
1232
|
+
info.description = ia[2];
|
|
1233
|
+
if (!info.location && typeof ia[3] === 'string')
|
|
1234
|
+
info.location = ia[3];
|
|
1235
|
+
if (typeof ia[4] === 'string')
|
|
1236
|
+
info.purpose = ia[4];
|
|
1237
|
+
if (typeof ia[5] === 'string')
|
|
1238
|
+
info.intendedUse = ia[5];
|
|
1239
|
+
if (typeof ia[7] === 'string')
|
|
1240
|
+
info.revision = ia[7];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (info.name || info.location || info.identification) {
|
|
1245
|
+
results.push(info);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
else if (typeUpper === 'IFCDOCUMENTINFORMATION') {
|
|
1249
|
+
// Direct IfcDocumentInformation (less common)
|
|
1250
|
+
const info = {
|
|
1251
|
+
identification: typeof attrs[0] === 'string' ? attrs[0] : undefined,
|
|
1252
|
+
name: typeof attrs[1] === 'string' ? attrs[1] : undefined,
|
|
1253
|
+
description: typeof attrs[2] === 'string' ? attrs[2] : undefined,
|
|
1254
|
+
location: typeof attrs[3] === 'string' ? attrs[3] : undefined,
|
|
1255
|
+
purpose: typeof attrs[4] === 'string' ? attrs[4] : undefined,
|
|
1256
|
+
intendedUse: typeof attrs[5] === 'string' ? attrs[5] : undefined,
|
|
1257
|
+
revision: typeof attrs[7] === 'string' ? attrs[7] : undefined,
|
|
1258
|
+
};
|
|
1259
|
+
if (info.name || info.location || info.identification) {
|
|
1260
|
+
results.push(info);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return results;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Extract structural relationships for a single entity ON-DEMAND.
|
|
1268
|
+
* Finds openings (VoidsElement), fills (FillsElement), groups (AssignsToGroup),
|
|
1269
|
+
* and path connections (ConnectsPathElements).
|
|
1270
|
+
*/
|
|
1271
|
+
export function extractRelationshipsOnDemand(store, entityId) {
|
|
1272
|
+
const result = {
|
|
1273
|
+
voids: [],
|
|
1274
|
+
fills: [],
|
|
1275
|
+
groups: [],
|
|
1276
|
+
connections: [],
|
|
1277
|
+
};
|
|
1278
|
+
if (!store.relationships)
|
|
1279
|
+
return result;
|
|
1280
|
+
const getEntityInfo = (id) => {
|
|
1281
|
+
const ref = store.entityIndex.byId.get(id);
|
|
1282
|
+
if (!ref)
|
|
1283
|
+
return { type: 'Unknown' };
|
|
1284
|
+
const name = store.entities?.getName(id);
|
|
1285
|
+
return { name: name || undefined, type: ref.type };
|
|
1286
|
+
};
|
|
1287
|
+
// VoidsElement: openings that void this element
|
|
1288
|
+
const voidsIds = store.relationships.getRelated(entityId, RelationshipType.VoidsElement, 'forward');
|
|
1289
|
+
for (const id of voidsIds) {
|
|
1290
|
+
const info = getEntityInfo(id);
|
|
1291
|
+
result.voids.push({ id, ...info });
|
|
1292
|
+
}
|
|
1293
|
+
// FillsElement: this element fills an opening
|
|
1294
|
+
const fillsIds = store.relationships.getRelated(entityId, RelationshipType.FillsElement, 'inverse');
|
|
1295
|
+
for (const id of fillsIds) {
|
|
1296
|
+
const info = getEntityInfo(id);
|
|
1297
|
+
result.fills.push({ id, ...info });
|
|
1298
|
+
}
|
|
1299
|
+
// AssignsToGroup: groups this element belongs to
|
|
1300
|
+
const groupIds = store.relationships.getRelated(entityId, RelationshipType.AssignsToGroup, 'inverse');
|
|
1301
|
+
for (const id of groupIds) {
|
|
1302
|
+
const name = store.entities?.getName(id);
|
|
1303
|
+
result.groups.push({ id, name: name || undefined });
|
|
1304
|
+
}
|
|
1305
|
+
// ConnectsPathElements: connected walls
|
|
1306
|
+
const connectedIds = store.relationships.getRelated(entityId, RelationshipType.ConnectsPathElements, 'forward');
|
|
1307
|
+
const connectedInverseIds = store.relationships.getRelated(entityId, RelationshipType.ConnectsPathElements, 'inverse');
|
|
1308
|
+
const allConnected = new Set([...connectedIds, ...connectedInverseIds]);
|
|
1309
|
+
allConnected.delete(entityId);
|
|
1310
|
+
for (const id of allConnected) {
|
|
1311
|
+
const info = getEntityInfo(id);
|
|
1312
|
+
result.connections.push({ id, ...info });
|
|
1313
|
+
}
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1316
|
+
// ============================================================================
|
|
1317
|
+
// On-Demand Georeferencing Extraction
|
|
1318
|
+
// ============================================================================
|
|
1319
|
+
import { extractGeoreferencing as extractGeorefFromEntities } from './georef-extractor.js';
|
|
1320
|
+
/**
|
|
1321
|
+
* Extract georeferencing info from on-demand store (source buffer + entityIndex).
|
|
1322
|
+
* Bridges to the entity-based georef extractor by resolving entities lazily.
|
|
1323
|
+
*/
|
|
1324
|
+
export function extractGeoreferencingOnDemand(store) {
|
|
1325
|
+
if (!store.source?.length || !store.entityIndex)
|
|
1326
|
+
return null;
|
|
1327
|
+
const extractor = new EntityExtractor(store.source);
|
|
1328
|
+
const { byId, byType } = store.entityIndex;
|
|
1329
|
+
// Build a lightweight entity map for just the georef-related types
|
|
1330
|
+
const entityMap = new Map();
|
|
1331
|
+
const typeMap = new Map();
|
|
1332
|
+
for (const typeName of ['IFCMAPCONVERSION', 'IFCPROJECTEDCRS']) {
|
|
1333
|
+
const ids = byType.get(typeName);
|
|
1334
|
+
if (!ids?.length)
|
|
1335
|
+
continue;
|
|
1336
|
+
// Use mixed-case for the georef extractor's type lookup
|
|
1337
|
+
const displayName = typeName === 'IFCMAPCONVERSION' ? 'IfcMapConversion' : 'IfcProjectedCRS';
|
|
1338
|
+
typeMap.set(displayName, ids);
|
|
1339
|
+
for (const id of ids) {
|
|
1340
|
+
const ref = byId.get(id);
|
|
1341
|
+
if (!ref)
|
|
1342
|
+
continue;
|
|
1343
|
+
const entity = extractor.extractEntity(ref);
|
|
1344
|
+
if (entity) {
|
|
1345
|
+
entityMap.set(id, entity);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (entityMap.size === 0)
|
|
1350
|
+
return null;
|
|
1351
|
+
// Cast to IfcEntity (they share the same shape)
|
|
1352
|
+
return extractGeorefFromEntities(entityMap, typeMap);
|
|
491
1353
|
}
|
|
492
1354
|
//# sourceMappingURL=columnar-parser.js.map
|