@ifc-lite/parser 2.1.0 → 2.1.1

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 +19 -814
  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 +1 -1
@@ -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',
@@ -126,10 +128,21 @@ export class ColumnarParser {
126
128
  const propertyTableBuilder = new PropertyTableBuilder(strings);
127
129
  const quantityTableBuilder = new QuantityTableBuilder(strings);
128
130
  const relationshipGraphBuilder = new RelationshipGraphBuilder();
129
- // Build entity index early (needed for property relationship lookup)
131
+ // Build compact entity index (typed arrays instead of Map for ~3x memory reduction)
132
+ const compactByIdIndex = buildCompactEntityIndex(entityRefs);
133
+ // Also build byType index (Map<string, number[]>)
134
+ const byType = new Map();
135
+ for (const ref of entityRefs) {
136
+ let typeList = byType.get(ref.type);
137
+ if (!typeList) {
138
+ typeList = [];
139
+ byType.set(ref.type, typeList);
140
+ }
141
+ typeList.push(ref.expressId);
142
+ }
130
143
  const entityIndex = {
131
- byId: new Map(),
132
- byType: new Map(),
144
+ byId: compactByIdIndex,
145
+ byType,
133
146
  };
134
147
  // First pass: collect spatial, geometry, relationship, property, and type refs for targeted parsing
135
148
  const spatialRefs = [];
@@ -140,14 +153,6 @@ export class ColumnarParser {
140
153
  const associationRelRefs = [];
141
154
  const typeObjectRefs = [];
142
155
  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
156
  // Categorize refs for targeted parsing
152
157
  const typeUpper = ref.type.toUpperCase();
153
158
  if (SPATIAL_TYPES.has(typeUpper)) {
@@ -627,806 +632,6 @@ export function extractAllEntityAttributes(store, entityId) {
627
632
  }
628
633
  return result;
629
634
  }
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
- }
635
+ // Re-export on-demand extraction functions from focused module
636
+ export { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, parsePropertyValue, extractPsetsFromIds, } from './on-demand-extractors.js';
1432
637
  //# sourceMappingURL=columnar-parser.js.map