@ifc-lite/parser 2.1.0 → 2.1.2

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 (40) hide show
  1. package/dist/attribute-helpers.d.ts +11 -0
  2. package/dist/attribute-helpers.d.ts.map +1 -0
  3. package/dist/attribute-helpers.js +63 -0
  4. package/dist/attribute-helpers.js.map +1 -0
  5. package/dist/classification-extractor.d.ts.map +1 -1
  6. package/dist/classification-extractor.js +1 -32
  7. package/dist/classification-extractor.js.map +1 -1
  8. package/dist/columnar-parser.d.ts +17 -148
  9. package/dist/columnar-parser.d.ts.map +1 -1
  10. package/dist/columnar-parser.js +54 -818
  11. package/dist/columnar-parser.js.map +1 -1
  12. package/dist/compact-entity-index.d.ts +95 -0
  13. package/dist/compact-entity-index.d.ts.map +1 -0
  14. package/dist/compact-entity-index.js +234 -0
  15. package/dist/compact-entity-index.js.map +1 -0
  16. package/dist/georef-extractor.d.ts.map +1 -1
  17. package/dist/georef-extractor.js +1 -29
  18. package/dist/georef-extractor.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/material-extractor.d.ts.map +1 -1
  24. package/dist/material-extractor.js +1 -47
  25. package/dist/material-extractor.js.map +1 -1
  26. package/dist/on-demand-extractors.d.ts +188 -0
  27. package/dist/on-demand-extractors.d.ts.map +1 -0
  28. package/dist/on-demand-extractors.js +829 -0
  29. package/dist/on-demand-extractors.js.map +1 -0
  30. package/dist/opfs-source-buffer.d.ts +70 -0
  31. package/dist/opfs-source-buffer.d.ts.map +1 -0
  32. package/dist/opfs-source-buffer.js +166 -0
  33. package/dist/opfs-source-buffer.js.map +1 -0
  34. package/dist/spatial-hierarchy-builder.d.ts +3 -1
  35. package/dist/spatial-hierarchy-builder.d.ts.map +1 -1
  36. package/dist/spatial-hierarchy-builder.js.map +1 -1
  37. package/dist/unit-extractor.d.ts +3 -1
  38. package/dist/unit-extractor.d.ts.map +1 -1
  39. package/dist/unit-extractor.js.map +1 -1
  40. package/package.json +2 -2
@@ -5,7 +5,9 @@ import { SpatialHierarchyBuilder } from './spatial-hierarchy-builder.js';
5
5
  import { EntityExtractor } from './entity-extractor.js';
6
6
  import { extractLengthUnitScale } from './unit-extractor.js';
7
7
  import { getAttributeNames } from './ifc-schema.js';
8
- import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, PropertyValueType, } from '@ifc-lite/data';
8
+ import { parsePropertyValue } from './on-demand-extractors.js';
9
+ import { buildCompactEntityIndex } from './compact-entity-index.js';
10
+ import { StringTable, EntityTableBuilder, PropertyTableBuilder, QuantityTableBuilder, RelationshipGraphBuilder, RelationshipType, QuantityType, } from '@ifc-lite/data';
9
11
  // Pre-computed type sets for O(1) lookups
10
12
  const GEOMETRY_TYPES = new Set([
11
13
  'IFCWALL', 'IFCWALLSTANDARDCASE', 'IFCDOOR', 'IFCWINDOW', 'IFCSLAB',
@@ -60,10 +62,16 @@ const QUANTITY_TYPE_MAP = {
60
62
  const SPATIAL_TYPES = new Set([
61
63
  'IFCPROJECT', 'IFCSITE', 'IFCBUILDING', 'IFCBUILDINGSTOREY', 'IFCSPACE',
62
64
  ]);
63
- // Relationship types needed for hierarchy
65
+ // Relationship types needed for hierarchy and structural relationships
64
66
  const HIERARCHY_REL_TYPES = new Set([
65
67
  'IFCRELAGGREGATES', 'IFCRELCONTAINEDINSPATIALSTRUCTURE',
66
68
  'IFCRELDEFINESBYTYPE',
69
+ // Structural relationships (voids, fills, connections, groups)
70
+ 'IFCRELVOIDSELEMENT', 'IFCRELFILLSELEMENT',
71
+ 'IFCRELCONNECTSPATHELEMENTS', 'IFCRELCONNECTSELEMENTS',
72
+ 'IFCRELSPACEBOUNDARY',
73
+ 'IFCRELASSIGNSTOGROUP', 'IFCRELASSIGNSTOPRODUCT',
74
+ 'IFCRELREFERENCEDINSPATIALSTRUCTURE',
67
75
  ]);
68
76
  // Relationship types for on-demand property loading
69
77
  const PROPERTY_REL_TYPES = new Set([
@@ -126,10 +134,21 @@ export class ColumnarParser {
126
134
  const propertyTableBuilder = new PropertyTableBuilder(strings);
127
135
  const quantityTableBuilder = new QuantityTableBuilder(strings);
128
136
  const relationshipGraphBuilder = new RelationshipGraphBuilder();
129
- // Build entity index early (needed for property relationship lookup)
137
+ // Build compact entity index (typed arrays instead of Map for ~3x memory reduction)
138
+ const compactByIdIndex = buildCompactEntityIndex(entityRefs);
139
+ // Also build byType index (Map<string, number[]>)
140
+ const byType = new Map();
141
+ for (const ref of entityRefs) {
142
+ let typeList = byType.get(ref.type);
143
+ if (!typeList) {
144
+ typeList = [];
145
+ byType.set(ref.type, typeList);
146
+ }
147
+ typeList.push(ref.expressId);
148
+ }
130
149
  const entityIndex = {
131
- byId: new Map(),
132
- byType: new Map(),
150
+ byId: compactByIdIndex,
151
+ byType,
133
152
  };
134
153
  // First pass: collect spatial, geometry, relationship, property, and type refs for targeted parsing
135
154
  const spatialRefs = [];
@@ -140,14 +159,6 @@ export class ColumnarParser {
140
159
  const associationRelRefs = [];
141
160
  const typeObjectRefs = [];
142
161
  for (const ref of entityRefs) {
143
- // Build entity index
144
- entityIndex.byId.set(ref.expressId, ref);
145
- let typeList = entityIndex.byType.get(ref.type);
146
- if (!typeList) {
147
- typeList = [];
148
- entityIndex.byType.set(ref.type, typeList);
149
- }
150
- typeList.push(ref.expressId);
151
162
  // Categorize refs for targeted parsing
152
163
  const typeUpper = ref.type.toUpperCase();
153
164
  if (SPATIAL_TYPES.has(typeUpper)) {
@@ -423,21 +434,46 @@ export class ColumnarParser {
423
434
  return null;
424
435
  let relatingObject;
425
436
  let relatedObjects;
426
- if (typeUpper === 'IFCRELDEFINESBYPROPERTIES' || typeUpper === 'IFCRELDEFINESBYTYPE' || typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE') {
437
+ if (typeUpper === 'IFCRELDEFINESBYPROPERTIES' || typeUpper === 'IFCRELDEFINESBYTYPE'
438
+ || typeUpper === 'IFCRELCONTAINEDINSPATIALSTRUCTURE' || typeUpper === 'IFCRELREFERENCEDINSPATIALSTRUCTURE') {
439
+ // [4]=RelatedObjects/RelatedElements, [5]=RelatingPropertyDef/Type/Structure
427
440
  relatedObjects = attrs[4];
428
441
  relatingObject = attrs[5];
429
442
  }
443
+ else if (typeUpper === 'IFCRELASSIGNSTOGROUP' || typeUpper === 'IFCRELASSIGNSTOPRODUCT') {
444
+ // IfcRelAssigns subtypes: [4]=RelatedObjects, [5]=RelatedObjectsType, [6]=RelatingGroup/Product
445
+ relatedObjects = attrs[4];
446
+ relatingObject = attrs.length > 6 ? attrs[6] : undefined;
447
+ }
448
+ else if (typeUpper === 'IFCRELCONNECTSELEMENTS' || typeUpper === 'IFCRELCONNECTSPATHELEMENTS') {
449
+ // [4]=ConnectionGeometry, [5]=RelatingElement, [6]=RelatedElement
450
+ relatingObject = attrs[5];
451
+ relatedObjects = attrs.length > 6 ? attrs[6] : undefined;
452
+ }
430
453
  else {
454
+ // IfcRelAggregates, IfcRelVoidsElement, IfcRelFillsElement, IfcRelSpaceBoundary, etc.
455
+ // [4]=RelatingObject, [5]=RelatedObject(s)
431
456
  relatingObject = attrs[4];
432
457
  relatedObjects = attrs[5];
433
458
  }
434
- if (typeof relatingObject !== 'number' || !Array.isArray(relatedObjects)) {
459
+ if (typeof relatingObject !== 'number') {
460
+ return null;
461
+ }
462
+ // Handle both 1-to-many (array) and 1-to-1 (single ref) relationships
463
+ let relatedArray;
464
+ if (Array.isArray(relatedObjects)) {
465
+ relatedArray = relatedObjects.filter((id) => typeof id === 'number');
466
+ }
467
+ else if (typeof relatedObjects === 'number') {
468
+ relatedArray = [relatedObjects];
469
+ }
470
+ else {
435
471
  return null;
436
472
  }
437
473
  return {
438
474
  type: entity.type,
439
475
  relatingObject,
440
- relatedObjects: relatedObjects.filter((id) => typeof id === 'number'),
476
+ relatedObjects: relatedArray,
441
477
  };
442
478
  }
443
479
  /**
@@ -627,806 +663,6 @@ export function extractAllEntityAttributes(store, entityId) {
627
663
  }
628
664
  return result;
629
665
  }
630
- /**
631
- * Extract classifications for a single entity ON-DEMAND.
632
- * Uses the onDemandClassificationMap built during parsing.
633
- * Falls back to relationship graph when on-demand map is not available (e.g., server-loaded models).
634
- * Also checks type-level associations via IfcRelDefinesByType.
635
- * Returns an array of classification references with system info.
636
- */
637
- export function extractClassificationsOnDemand(store, entityId) {
638
- let classRefIds;
639
- if (store.onDemandClassificationMap) {
640
- classRefIds = store.onDemandClassificationMap.get(entityId);
641
- }
642
- else if (store.relationships) {
643
- // Fallback: use relationship graph (server-loaded models)
644
- const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesClassification, 'inverse');
645
- if (related.length > 0)
646
- classRefIds = related;
647
- }
648
- // Also check type-level classifications via IfcRelDefinesByType
649
- if (store.relationships) {
650
- const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
651
- for (const typeId of typeIds) {
652
- let typeClassRefs;
653
- if (store.onDemandClassificationMap) {
654
- typeClassRefs = store.onDemandClassificationMap.get(typeId);
655
- }
656
- else {
657
- const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesClassification, 'inverse');
658
- if (related.length > 0)
659
- typeClassRefs = related;
660
- }
661
- if (typeClassRefs && typeClassRefs.length > 0) {
662
- classRefIds = classRefIds ? [...classRefIds, ...typeClassRefs] : [...typeClassRefs];
663
- }
664
- }
665
- }
666
- if (!classRefIds || classRefIds.length === 0)
667
- return [];
668
- if (!store.source?.length)
669
- return [];
670
- const extractor = new EntityExtractor(store.source);
671
- const results = [];
672
- for (const classRefId of classRefIds) {
673
- const ref = store.entityIndex.byId.get(classRefId);
674
- if (!ref)
675
- continue;
676
- const entity = extractor.extractEntity(ref);
677
- if (!entity)
678
- continue;
679
- const typeUpper = entity.type.toUpperCase();
680
- const attrs = entity.attributes || [];
681
- if (typeUpper === 'IFCCLASSIFICATIONREFERENCE') {
682
- // IfcClassificationReference: [Location, Identification, Name, ReferencedSource, Description, Sort]
683
- const info = {
684
- location: typeof attrs[0] === 'string' ? attrs[0] : undefined,
685
- identification: typeof attrs[1] === 'string' ? attrs[1] : undefined,
686
- name: typeof attrs[2] === 'string' ? attrs[2] : undefined,
687
- description: typeof attrs[4] === 'string' ? attrs[4] : undefined,
688
- };
689
- // Walk up to find the classification system name
690
- const referencedSourceId = typeof attrs[3] === 'number' ? attrs[3] : undefined;
691
- if (referencedSourceId) {
692
- const path = walkClassificationChain(store, extractor, referencedSourceId);
693
- info.system = path.systemName;
694
- info.path = path.codes;
695
- }
696
- results.push(info);
697
- }
698
- else if (typeUpper === 'IFCCLASSIFICATION') {
699
- // IfcClassification: [Source, Edition, EditionDate, Name, Description, Location, ReferenceTokens]
700
- results.push({
701
- system: typeof attrs[3] === 'string' ? attrs[3] : undefined,
702
- name: typeof attrs[3] === 'string' ? attrs[3] : undefined,
703
- description: typeof attrs[4] === 'string' ? attrs[4] : undefined,
704
- location: typeof attrs[5] === 'string' ? attrs[5] : undefined,
705
- });
706
- }
707
- }
708
- return results;
709
- }
710
- /**
711
- * Walk up the IfcClassificationReference chain to find the root IfcClassification system.
712
- */
713
- function walkClassificationChain(store, extractor, startId) {
714
- const codes = [];
715
- let currentId = startId;
716
- const visited = new Set();
717
- while (currentId !== undefined && !visited.has(currentId)) {
718
- visited.add(currentId);
719
- const ref = store.entityIndex.byId.get(currentId);
720
- if (!ref)
721
- break;
722
- const entity = extractor.extractEntity(ref);
723
- if (!entity)
724
- break;
725
- const typeUpper = entity.type.toUpperCase();
726
- const attrs = entity.attributes || [];
727
- if (typeUpper === 'IFCCLASSIFICATION') {
728
- // Root: IfcClassification [Source, Edition, EditionDate, Name, ...]
729
- const systemName = typeof attrs[3] === 'string' ? attrs[3] : undefined;
730
- return { systemName, codes };
731
- }
732
- if (typeUpper === 'IFCCLASSIFICATIONREFERENCE') {
733
- // IfcClassificationReference [Location, Identification, Name, ReferencedSource, ...]
734
- const code = typeof attrs[1] === 'string' ? attrs[1] :
735
- typeof attrs[2] === 'string' ? attrs[2] : undefined;
736
- if (code)
737
- codes.unshift(code);
738
- currentId = typeof attrs[3] === 'number' ? attrs[3] : undefined;
739
- }
740
- else {
741
- break;
742
- }
743
- }
744
- return { codes };
745
- }
746
- /**
747
- * Extract materials for a single entity ON-DEMAND.
748
- * Uses the onDemandMaterialMap built during parsing.
749
- * Falls back to relationship graph when on-demand map is not available (e.g., server-loaded models).
750
- * Also checks type-level material assignments via IfcRelDefinesByType.
751
- * Resolves the full material structure (layers, profiles, constituents, lists).
752
- */
753
- export function extractMaterialsOnDemand(store, entityId) {
754
- let materialId;
755
- if (store.onDemandMaterialMap) {
756
- materialId = store.onDemandMaterialMap.get(entityId);
757
- }
758
- else if (store.relationships) {
759
- // Fallback: use relationship graph (server-loaded models)
760
- const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesMaterial, 'inverse');
761
- if (related.length > 0)
762
- materialId = related[0];
763
- }
764
- // Check type-level material if occurrence has none
765
- if (materialId === undefined && store.relationships) {
766
- const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
767
- for (const typeId of typeIds) {
768
- if (store.onDemandMaterialMap) {
769
- materialId = store.onDemandMaterialMap.get(typeId);
770
- }
771
- else {
772
- const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesMaterial, 'inverse');
773
- if (related.length > 0)
774
- materialId = related[0];
775
- }
776
- if (materialId !== undefined)
777
- break;
778
- }
779
- }
780
- if (materialId === undefined)
781
- return null;
782
- if (!store.source?.length)
783
- return null;
784
- const extractor = new EntityExtractor(store.source);
785
- return resolveMaterial(store, extractor, materialId, new Set());
786
- }
787
- /**
788
- * Resolve a material entity by ID, handling all IFC material types.
789
- * Uses visited set to prevent infinite recursion on cyclic *Usage references.
790
- */
791
- function resolveMaterial(store, extractor, materialId, visited = new Set()) {
792
- if (visited.has(materialId))
793
- return null;
794
- visited.add(materialId);
795
- const ref = store.entityIndex.byId.get(materialId);
796
- if (!ref)
797
- return null;
798
- const entity = extractor.extractEntity(ref);
799
- if (!entity)
800
- return null;
801
- const typeUpper = entity.type.toUpperCase();
802
- const attrs = entity.attributes || [];
803
- switch (typeUpper) {
804
- case 'IFCMATERIAL': {
805
- // IfcMaterial: [Name, Description, Category]
806
- return {
807
- type: 'Material',
808
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
809
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
810
- };
811
- }
812
- case 'IFCMATERIALLAYERSET': {
813
- // IfcMaterialLayerSet: [MaterialLayers, LayerSetName, Description]
814
- const layerIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
815
- const layers = [];
816
- for (const layerId of layerIds) {
817
- const layerRef = store.entityIndex.byId.get(layerId);
818
- if (!layerRef)
819
- continue;
820
- const layerEntity = extractor.extractEntity(layerRef);
821
- if (!layerEntity)
822
- continue;
823
- const la = layerEntity.attributes || [];
824
- // IfcMaterialLayer: [Material, LayerThickness, IsVentilated, Name, Description, Category, Priority]
825
- const matId = typeof la[0] === 'number' ? la[0] : undefined;
826
- let materialName;
827
- if (matId) {
828
- const matRef = store.entityIndex.byId.get(matId);
829
- if (matRef) {
830
- const matEntity = extractor.extractEntity(matRef);
831
- if (matEntity) {
832
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
833
- }
834
- }
835
- }
836
- layers.push({
837
- materialName,
838
- thickness: typeof la[1] === 'number' ? la[1] : undefined,
839
- isVentilated: la[2] === true || la[2] === '.T.',
840
- name: typeof la[3] === 'string' ? la[3] : undefined,
841
- category: typeof la[5] === 'string' ? la[5] : undefined,
842
- });
843
- }
844
- return {
845
- type: 'MaterialLayerSet',
846
- name: typeof attrs[1] === 'string' ? attrs[1] : undefined,
847
- description: typeof attrs[2] === 'string' ? attrs[2] : undefined,
848
- layers,
849
- };
850
- }
851
- case 'IFCMATERIALPROFILESET': {
852
- // IfcMaterialProfileSet: [Name, Description, MaterialProfiles, CompositeProfile]
853
- const profileIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
854
- const profiles = [];
855
- for (const profId of profileIds) {
856
- const profRef = store.entityIndex.byId.get(profId);
857
- if (!profRef)
858
- continue;
859
- const profEntity = extractor.extractEntity(profRef);
860
- if (!profEntity)
861
- continue;
862
- const pa = profEntity.attributes || [];
863
- // IfcMaterialProfile: [Name, Description, Material, Profile, Priority, Category]
864
- const matId = typeof pa[2] === 'number' ? pa[2] : undefined;
865
- let materialName;
866
- if (matId) {
867
- const matRef = store.entityIndex.byId.get(matId);
868
- if (matRef) {
869
- const matEntity = extractor.extractEntity(matRef);
870
- if (matEntity) {
871
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
872
- }
873
- }
874
- }
875
- profiles.push({
876
- materialName,
877
- name: typeof pa[0] === 'string' ? pa[0] : undefined,
878
- category: typeof pa[5] === 'string' ? pa[5] : undefined,
879
- });
880
- }
881
- return {
882
- type: 'MaterialProfileSet',
883
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
884
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
885
- profiles,
886
- };
887
- }
888
- case 'IFCMATERIALCONSTITUENTSET': {
889
- // IfcMaterialConstituentSet: [Name, Description, MaterialConstituents]
890
- const constituentIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
891
- const constituents = [];
892
- for (const constId of constituentIds) {
893
- const constRef = store.entityIndex.byId.get(constId);
894
- if (!constRef)
895
- continue;
896
- const constEntity = extractor.extractEntity(constRef);
897
- if (!constEntity)
898
- continue;
899
- const ca = constEntity.attributes || [];
900
- // IfcMaterialConstituent: [Name, Description, Material, Fraction, Category]
901
- const matId = typeof ca[2] === 'number' ? ca[2] : undefined;
902
- let materialName;
903
- if (matId) {
904
- const matRef = store.entityIndex.byId.get(matId);
905
- if (matRef) {
906
- const matEntity = extractor.extractEntity(matRef);
907
- if (matEntity) {
908
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
909
- }
910
- }
911
- }
912
- constituents.push({
913
- materialName,
914
- name: typeof ca[0] === 'string' ? ca[0] : undefined,
915
- fraction: typeof ca[3] === 'number' ? ca[3] : undefined,
916
- category: typeof ca[4] === 'string' ? ca[4] : undefined,
917
- });
918
- }
919
- return {
920
- type: 'MaterialConstituentSet',
921
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
922
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
923
- constituents,
924
- };
925
- }
926
- case 'IFCMATERIALLIST': {
927
- // IfcMaterialList: [Materials]
928
- const matIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
929
- const materials = [];
930
- for (const matId of matIds) {
931
- const matRef = store.entityIndex.byId.get(matId);
932
- if (!matRef)
933
- continue;
934
- const matEntity = extractor.extractEntity(matRef);
935
- if (matEntity) {
936
- const name = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : `Material #${matId}`;
937
- materials.push(name);
938
- }
939
- }
940
- return {
941
- type: 'MaterialList',
942
- materials,
943
- };
944
- }
945
- case 'IFCMATERIALLAYERSETUSAGE': {
946
- // IfcMaterialLayerSetUsage: [ForLayerSet, LayerSetDirection, DirectionSense, OffsetFromReferenceLine, ...]
947
- const layerSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
948
- if (layerSetId) {
949
- return resolveMaterial(store, extractor, layerSetId, visited);
950
- }
951
- return null;
952
- }
953
- case 'IFCMATERIALPROFILESETUSAGE': {
954
- // IfcMaterialProfileSetUsage: [ForProfileSet, ...]
955
- const profileSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
956
- if (profileSetId) {
957
- return resolveMaterial(store, extractor, profileSetId, visited);
958
- }
959
- return null;
960
- }
961
- default:
962
- return null;
963
- }
964
- }
965
- /**
966
- * Parse a property entity's value based on its IFC type.
967
- * Handles all 6 IfcProperty subtypes:
968
- * - IfcPropertySingleValue: direct value
969
- * - IfcPropertyEnumeratedValue: list of enum values → joined string
970
- * - IfcPropertyBoundedValue: upper/lower bounds → "value [min – max]"
971
- * - IfcPropertyListValue: list of values → joined string
972
- * - IfcPropertyTableValue: defining/defined value pairs → "Table(N rows)"
973
- * - IfcPropertyReferenceValue: entity reference → "Reference #ID"
974
- */
975
- function parsePropertyValue(propEntity) {
976
- const attrs = propEntity.attributes || [];
977
- const typeUpper = propEntity.type.toUpperCase();
978
- switch (typeUpper) {
979
- case 'IFCPROPERTYENUMERATEDVALUE': {
980
- // [Name, Description, EnumerationValues (list), EnumerationReference]
981
- const enumValues = attrs[2];
982
- if (Array.isArray(enumValues)) {
983
- const values = enumValues.map(v => {
984
- if (Array.isArray(v) && v.length === 2)
985
- return String(v[1]); // Typed value
986
- return String(v);
987
- }).filter(v => v !== 'null' && v !== 'undefined');
988
- return { type: 0, value: values.join(', ') };
989
- }
990
- return { type: 0, value: null };
991
- }
992
- case 'IFCPROPERTYBOUNDEDVALUE': {
993
- // [Name, Description, UpperBoundValue, LowerBoundValue, Unit, SetPointValue]
994
- const upper = extractNumericValue(attrs[2]);
995
- const lower = extractNumericValue(attrs[3]);
996
- const setPoint = extractNumericValue(attrs[5]);
997
- const displayValue = setPoint ?? upper ?? lower;
998
- let display = displayValue != null ? String(displayValue) : '';
999
- if (lower != null && upper != null) {
1000
- display += ` [${lower} – ${upper}]`;
1001
- }
1002
- return { type: displayValue != null ? 1 : 0, value: display || null };
1003
- }
1004
- case 'IFCPROPERTYLISTVALUE': {
1005
- // [Name, Description, ListValues (list), Unit]
1006
- const listValues = attrs[2];
1007
- if (Array.isArray(listValues)) {
1008
- const values = listValues.map(v => {
1009
- if (Array.isArray(v) && v.length === 2)
1010
- return String(v[1]);
1011
- return String(v);
1012
- }).filter(v => v !== 'null' && v !== 'undefined');
1013
- return { type: 0, value: values.join(', ') };
1014
- }
1015
- return { type: 0, value: null };
1016
- }
1017
- case 'IFCPROPERTYTABLEVALUE': {
1018
- // [Name, Description, DefiningValues, DefinedValues, ...]
1019
- const definingValues = attrs[2];
1020
- const definedValues = attrs[3];
1021
- const rowCount = Array.isArray(definingValues) ? definingValues.length : 0;
1022
- if (rowCount > 0 && Array.isArray(definedValues)) {
1023
- return { type: 0, value: `Table (${rowCount} rows)` };
1024
- }
1025
- return { type: 0, value: null };
1026
- }
1027
- case 'IFCPROPERTYREFERENCEVALUE': {
1028
- // [Name, Description, PropertyReference]
1029
- const refValue = attrs[2];
1030
- if (typeof refValue === 'number') {
1031
- return { type: 0, value: `#${refValue}` };
1032
- }
1033
- return { type: 0, value: null };
1034
- }
1035
- default: {
1036
- // IfcPropertySingleValue and fallback: [Name, Description, NominalValue, Unit]
1037
- const nominalValue = attrs[2];
1038
- let type = PropertyValueType.String;
1039
- let value = nominalValue;
1040
- // Handle typed values like IFCBOOLEAN(.T.), IFCREAL(1.5)
1041
- if (Array.isArray(nominalValue) && nominalValue.length === 2) {
1042
- const innerValue = nominalValue[1];
1043
- const typeName = String(nominalValue[0]).toUpperCase();
1044
- if (typeName.includes('BOOLEAN')) {
1045
- type = PropertyValueType.Boolean;
1046
- value = innerValue === '.T.' || innerValue === true;
1047
- }
1048
- else if (typeName.includes('LOGICAL')) {
1049
- type = PropertyValueType.Logical;
1050
- // Preserve .U. (unknown) as null; .T./.F. as boolean
1051
- if (innerValue === '.U.' || innerValue === '.X.') {
1052
- value = null;
1053
- }
1054
- else {
1055
- value = innerValue === '.T.' || innerValue === true;
1056
- }
1057
- }
1058
- else if (typeof innerValue === 'number') {
1059
- if (Number.isInteger(innerValue)) {
1060
- type = PropertyValueType.Integer;
1061
- }
1062
- else {
1063
- type = PropertyValueType.Real;
1064
- }
1065
- value = innerValue;
1066
- }
1067
- else {
1068
- type = PropertyValueType.String;
1069
- value = String(innerValue);
1070
- }
1071
- }
1072
- else if (typeof nominalValue === 'number') {
1073
- type = Number.isInteger(nominalValue) ? PropertyValueType.Integer : PropertyValueType.Real;
1074
- }
1075
- else if (typeof nominalValue === 'boolean') {
1076
- type = PropertyValueType.Boolean;
1077
- }
1078
- else if (nominalValue !== null && nominalValue !== undefined) {
1079
- value = String(nominalValue);
1080
- }
1081
- return { type, value };
1082
- }
1083
- }
1084
- }
1085
- /** Extract a numeric value from a possibly typed STEP value. */
1086
- function extractNumericValue(attr) {
1087
- if (typeof attr === 'number')
1088
- return attr;
1089
- if (Array.isArray(attr) && attr.length === 2 && typeof attr[1] === 'number')
1090
- return attr[1];
1091
- return null;
1092
- }
1093
- /**
1094
- * Extract property sets from a list of pset IDs using the entity index.
1095
- * Shared logic between instance-level and type-level property extraction.
1096
- */
1097
- function extractPsetsFromIds(store, extractor, psetIds) {
1098
- const result = [];
1099
- for (const psetId of psetIds) {
1100
- const psetRef = store.entityIndex.byId.get(psetId);
1101
- if (!psetRef)
1102
- continue;
1103
- // Only extract IFCPROPERTYSET entities (skip quantity sets etc.)
1104
- if (psetRef.type.toUpperCase() !== 'IFCPROPERTYSET')
1105
- continue;
1106
- const psetEntity = extractor.extractEntity(psetRef);
1107
- if (!psetEntity)
1108
- continue;
1109
- const psetAttrs = psetEntity.attributes || [];
1110
- const psetGlobalId = typeof psetAttrs[0] === 'string' ? psetAttrs[0] : undefined;
1111
- const psetName = typeof psetAttrs[2] === 'string' ? psetAttrs[2] : `PropertySet #${psetId}`;
1112
- const hasProperties = psetAttrs[4];
1113
- const properties = [];
1114
- if (Array.isArray(hasProperties)) {
1115
- for (const propRef of hasProperties) {
1116
- if (typeof propRef !== 'number')
1117
- continue;
1118
- const propEntityRef = store.entityIndex.byId.get(propRef);
1119
- if (!propEntityRef)
1120
- continue;
1121
- const propEntity = extractor.extractEntity(propEntityRef);
1122
- if (!propEntity)
1123
- continue;
1124
- const propAttrs = propEntity.attributes || [];
1125
- const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
1126
- if (!propName)
1127
- continue;
1128
- const parsed = parsePropertyValue(propEntity);
1129
- properties.push({ name: propName, type: parsed.type, value: parsed.value });
1130
- }
1131
- }
1132
- if (properties.length > 0 || psetName) {
1133
- result.push({ name: psetName, globalId: psetGlobalId, properties });
1134
- }
1135
- }
1136
- return result;
1137
- }
1138
- /**
1139
- * Extract type-level properties for a single entity ON-DEMAND.
1140
- * Finds the element's type via IfcRelDefinesByType, then extracts property sets from:
1141
- * 1. The type entity's HasPropertySets attribute (IFC2X3/IFC4: index 5 on IfcTypeObject)
1142
- * 2. The onDemandPropertyMap for the type entity (IFC4 IFCRELDEFINESBYPROPERTIES → type)
1143
- * Returns null if no type relationship exists.
1144
- */
1145
- export function extractTypePropertiesOnDemand(store, entityId) {
1146
- if (!store.relationships)
1147
- return null;
1148
- // Find type entity via DefinesByType relationship (inverse: element → type)
1149
- const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
1150
- if (typeIds.length === 0)
1151
- return null;
1152
- const typeId = typeIds[0]; // An element typically has one type
1153
- const typeRef = store.entityIndex.byId.get(typeId);
1154
- if (!typeRef)
1155
- return null;
1156
- if (!store.source?.length)
1157
- return null;
1158
- const extractor = new EntityExtractor(store.source);
1159
- // Get type name from entity
1160
- const typeEntity = extractor.extractEntity(typeRef);
1161
- const typeName = typeEntity && typeof typeEntity.attributes?.[2] === 'string'
1162
- ? typeEntity.attributes[2]
1163
- : typeRef.type;
1164
- const allPsets = [];
1165
- const seenPsetNames = new Set();
1166
- // Source 1: HasPropertySets attribute on the type entity (index 5 for IfcTypeObject subtypes)
1167
- // Works for both IFC2X3 and IFC4
1168
- if (typeEntity) {
1169
- const hasPropertySets = typeEntity.attributes?.[5];
1170
- if (Array.isArray(hasPropertySets)) {
1171
- const psetIds = hasPropertySets.filter((id) => typeof id === 'number');
1172
- const psets = extractPsetsFromIds(store, extractor, psetIds);
1173
- for (const pset of psets) {
1174
- seenPsetNames.add(pset.name);
1175
- allPsets.push(pset);
1176
- }
1177
- }
1178
- }
1179
- // Source 2: onDemandPropertyMap for the type entity (IFC4: via IFCRELDEFINESBYPROPERTIES)
1180
- if (store.onDemandPropertyMap) {
1181
- const typePsetIds = store.onDemandPropertyMap.get(typeId);
1182
- if (typePsetIds && typePsetIds.length > 0) {
1183
- const psets = extractPsetsFromIds(store, extractor, typePsetIds);
1184
- for (const pset of psets) {
1185
- if (!seenPsetNames.has(pset.name)) {
1186
- allPsets.push(pset);
1187
- }
1188
- }
1189
- }
1190
- }
1191
- if (allPsets.length === 0)
1192
- return null;
1193
- return {
1194
- typeName,
1195
- typeId,
1196
- properties: allPsets,
1197
- };
1198
- }
1199
- /**
1200
- * Extract properties from a type entity's own HasPropertySets attribute.
1201
- * Used when the type entity itself is selected (e.g., via "By Type" tree).
1202
- * Returns the type's own property sets from attribute index 5 + any via IfcRelDefinesByProperties.
1203
- */
1204
- export function extractTypeEntityOwnProperties(store, typeEntityId) {
1205
- const ref = store.entityIndex.byId.get(typeEntityId);
1206
- if (!ref || !store.source?.length)
1207
- return [];
1208
- const extractor = new EntityExtractor(store.source);
1209
- const typeEntity = extractor.extractEntity(ref);
1210
- if (!typeEntity)
1211
- return [];
1212
- const allPsets = [];
1213
- const seenPsetNames = new Set();
1214
- // Source 1: HasPropertySets attribute (index 5 for IfcTypeObject subtypes)
1215
- const hasPropertySets = typeEntity.attributes?.[5];
1216
- if (Array.isArray(hasPropertySets)) {
1217
- const psetIds = hasPropertySets.filter((id) => typeof id === 'number');
1218
- const psets = extractPsetsFromIds(store, extractor, psetIds);
1219
- for (const pset of psets) {
1220
- seenPsetNames.add(pset.name);
1221
- allPsets.push(pset);
1222
- }
1223
- }
1224
- // Source 2: onDemandPropertyMap (IFC4: via IFCRELDEFINESBYPROPERTIES)
1225
- if (store.onDemandPropertyMap) {
1226
- const typePsetIds = store.onDemandPropertyMap.get(typeEntityId);
1227
- if (typePsetIds && typePsetIds.length > 0) {
1228
- const psets = extractPsetsFromIds(store, extractor, typePsetIds);
1229
- for (const pset of psets) {
1230
- if (!seenPsetNames.has(pset.name)) {
1231
- allPsets.push(pset);
1232
- }
1233
- }
1234
- }
1235
- }
1236
- return allPsets;
1237
- }
1238
- /**
1239
- * Extract documents for a single entity ON-DEMAND.
1240
- * Uses the onDemandDocumentMap built during parsing.
1241
- * Falls back to relationship graph when on-demand map is not available.
1242
- * Also checks type-level documents via IfcRelDefinesByType.
1243
- * Returns an array of document info objects.
1244
- */
1245
- export function extractDocumentsOnDemand(store, entityId) {
1246
- let docRefIds;
1247
- if (store.onDemandDocumentMap) {
1248
- docRefIds = store.onDemandDocumentMap.get(entityId);
1249
- }
1250
- else if (store.relationships) {
1251
- const related = store.relationships.getRelated(entityId, RelationshipType.AssociatesDocument, 'inverse');
1252
- if (related.length > 0)
1253
- docRefIds = related;
1254
- }
1255
- // Also check type-level documents via IfcRelDefinesByType
1256
- if (store.relationships) {
1257
- const typeIds = store.relationships.getRelated(entityId, RelationshipType.DefinesByType, 'inverse');
1258
- for (const typeId of typeIds) {
1259
- let typeDocRefs;
1260
- if (store.onDemandDocumentMap) {
1261
- typeDocRefs = store.onDemandDocumentMap.get(typeId);
1262
- }
1263
- else {
1264
- const related = store.relationships.getRelated(typeId, RelationshipType.AssociatesDocument, 'inverse');
1265
- if (related.length > 0)
1266
- typeDocRefs = related;
1267
- }
1268
- if (typeDocRefs && typeDocRefs.length > 0) {
1269
- docRefIds = docRefIds ? [...docRefIds, ...typeDocRefs] : [...typeDocRefs];
1270
- }
1271
- }
1272
- }
1273
- if (!docRefIds || docRefIds.length === 0)
1274
- return [];
1275
- if (!store.source?.length)
1276
- return [];
1277
- const extractor = new EntityExtractor(store.source);
1278
- const results = [];
1279
- for (const docId of docRefIds) {
1280
- const docRef = store.entityIndex.byId.get(docId);
1281
- if (!docRef)
1282
- continue;
1283
- const docEntity = extractor.extractEntity(docRef);
1284
- if (!docEntity)
1285
- continue;
1286
- const typeUpper = docEntity.type.toUpperCase();
1287
- const attrs = docEntity.attributes || [];
1288
- if (typeUpper === 'IFCDOCUMENTREFERENCE') {
1289
- // IFC4: [Location, Identification, Name, Description, ReferencedDocument]
1290
- // IFC2X3: [Location, ItemReference, Name]
1291
- const info = {
1292
- location: typeof attrs[0] === 'string' ? attrs[0] : undefined,
1293
- identification: typeof attrs[1] === 'string' ? attrs[1] : undefined,
1294
- name: typeof attrs[2] === 'string' ? attrs[2] : undefined,
1295
- description: typeof attrs[3] === 'string' ? attrs[3] : undefined,
1296
- };
1297
- // Walk to IfcDocumentInformation if ReferencedDocument is set (IFC4 attr[4])
1298
- if (typeof attrs[4] === 'number') {
1299
- const docInfoRef = store.entityIndex.byId.get(attrs[4]);
1300
- if (docInfoRef) {
1301
- const docInfoEntity = extractor.extractEntity(docInfoRef);
1302
- if (docInfoEntity && docInfoEntity.type.toUpperCase() === 'IFCDOCUMENTINFORMATION') {
1303
- const ia = docInfoEntity.attributes || [];
1304
- // IfcDocumentInformation: [Identification, Name, Description, Location, Purpose, IntendedUse, Scope, Revision, ...]
1305
- if (!info.identification && typeof ia[0] === 'string')
1306
- info.identification = ia[0];
1307
- if (!info.name && typeof ia[1] === 'string')
1308
- info.name = ia[1];
1309
- if (!info.description && typeof ia[2] === 'string')
1310
- info.description = ia[2];
1311
- if (!info.location && typeof ia[3] === 'string')
1312
- info.location = ia[3];
1313
- if (typeof ia[4] === 'string')
1314
- info.purpose = ia[4];
1315
- if (typeof ia[5] === 'string')
1316
- info.intendedUse = ia[5];
1317
- if (typeof ia[7] === 'string')
1318
- info.revision = ia[7];
1319
- }
1320
- }
1321
- }
1322
- if (info.name || info.location || info.identification) {
1323
- results.push(info);
1324
- }
1325
- }
1326
- else if (typeUpper === 'IFCDOCUMENTINFORMATION') {
1327
- // Direct IfcDocumentInformation (less common)
1328
- const info = {
1329
- identification: typeof attrs[0] === 'string' ? attrs[0] : undefined,
1330
- name: typeof attrs[1] === 'string' ? attrs[1] : undefined,
1331
- description: typeof attrs[2] === 'string' ? attrs[2] : undefined,
1332
- location: typeof attrs[3] === 'string' ? attrs[3] : undefined,
1333
- purpose: typeof attrs[4] === 'string' ? attrs[4] : undefined,
1334
- intendedUse: typeof attrs[5] === 'string' ? attrs[5] : undefined,
1335
- revision: typeof attrs[7] === 'string' ? attrs[7] : undefined,
1336
- };
1337
- if (info.name || info.location || info.identification) {
1338
- results.push(info);
1339
- }
1340
- }
1341
- }
1342
- return results;
1343
- }
1344
- /**
1345
- * Extract structural relationships for a single entity ON-DEMAND.
1346
- * Finds openings (VoidsElement), fills (FillsElement), groups (AssignsToGroup),
1347
- * and path connections (ConnectsPathElements).
1348
- */
1349
- export function extractRelationshipsOnDemand(store, entityId) {
1350
- const result = {
1351
- voids: [],
1352
- fills: [],
1353
- groups: [],
1354
- connections: [],
1355
- };
1356
- if (!store.relationships)
1357
- return result;
1358
- const getEntityInfo = (id) => {
1359
- const ref = store.entityIndex.byId.get(id);
1360
- if (!ref)
1361
- return { type: 'Unknown' };
1362
- const name = store.entities?.getName(id);
1363
- return { name: name || undefined, type: ref.type };
1364
- };
1365
- // VoidsElement: openings that void this element
1366
- const voidsIds = store.relationships.getRelated(entityId, RelationshipType.VoidsElement, 'forward');
1367
- for (const id of voidsIds) {
1368
- const info = getEntityInfo(id);
1369
- result.voids.push({ id, ...info });
1370
- }
1371
- // FillsElement: this element fills an opening
1372
- const fillsIds = store.relationships.getRelated(entityId, RelationshipType.FillsElement, 'inverse');
1373
- for (const id of fillsIds) {
1374
- const info = getEntityInfo(id);
1375
- result.fills.push({ id, ...info });
1376
- }
1377
- // AssignsToGroup: groups this element belongs to
1378
- const groupIds = store.relationships.getRelated(entityId, RelationshipType.AssignsToGroup, 'inverse');
1379
- for (const id of groupIds) {
1380
- const name = store.entities?.getName(id);
1381
- result.groups.push({ id, name: name || undefined });
1382
- }
1383
- // ConnectsPathElements: connected walls
1384
- const connectedIds = store.relationships.getRelated(entityId, RelationshipType.ConnectsPathElements, 'forward');
1385
- const connectedInverseIds = store.relationships.getRelated(entityId, RelationshipType.ConnectsPathElements, 'inverse');
1386
- const allConnected = new Set([...connectedIds, ...connectedInverseIds]);
1387
- allConnected.delete(entityId);
1388
- for (const id of allConnected) {
1389
- const info = getEntityInfo(id);
1390
- result.connections.push({ id, ...info });
1391
- }
1392
- return result;
1393
- }
1394
- // ============================================================================
1395
- // On-Demand Georeferencing Extraction
1396
- // ============================================================================
1397
- import { extractGeoreferencing as extractGeorefFromEntities } from './georef-extractor.js';
1398
- /**
1399
- * Extract georeferencing info from on-demand store (source buffer + entityIndex).
1400
- * Bridges to the entity-based georef extractor by resolving entities lazily.
1401
- */
1402
- export function extractGeoreferencingOnDemand(store) {
1403
- if (!store.source?.length || !store.entityIndex)
1404
- return null;
1405
- const extractor = new EntityExtractor(store.source);
1406
- const { byId, byType } = store.entityIndex;
1407
- // Build a lightweight entity map for just the georef-related types
1408
- const entityMap = new Map();
1409
- const typeMap = new Map();
1410
- for (const typeName of ['IFCMAPCONVERSION', 'IFCPROJECTEDCRS']) {
1411
- const ids = byType.get(typeName);
1412
- if (!ids?.length)
1413
- continue;
1414
- // Use mixed-case for the georef extractor's type lookup
1415
- const displayName = typeName === 'IFCMAPCONVERSION' ? 'IfcMapConversion' : 'IfcProjectedCRS';
1416
- typeMap.set(displayName, ids);
1417
- for (const id of ids) {
1418
- const ref = byId.get(id);
1419
- if (!ref)
1420
- continue;
1421
- const entity = extractor.extractEntity(ref);
1422
- if (entity) {
1423
- entityMap.set(id, entity);
1424
- }
1425
- }
1426
- }
1427
- if (entityMap.size === 0)
1428
- return null;
1429
- // Cast to IfcEntity (they share the same shape)
1430
- return extractGeorefFromEntities(entityMap, typeMap);
1431
- }
666
+ // Re-export on-demand extraction functions from focused module
667
+ export { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, parsePropertyValue, extractPsetsFromIds, } from './on-demand-extractors.js';
1432
668
  //# sourceMappingURL=columnar-parser.js.map