@ifc-lite/parser 4.3.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/attribute-helpers.d.ts +38 -4
  2. package/dist/attribute-helpers.d.ts.map +1 -1
  3. package/dist/attribute-helpers.js +71 -4
  4. package/dist/attribute-helpers.js.map +1 -1
  5. package/dist/columnar-parser-attributes.d.ts.map +1 -1
  6. package/dist/columnar-parser-attributes.js +35 -5
  7. package/dist/columnar-parser-attributes.js.map +1 -1
  8. package/dist/columnar-parser-indexes.d.ts.map +1 -1
  9. package/dist/columnar-parser-indexes.js +17 -2
  10. package/dist/columnar-parser-indexes.js.map +1 -1
  11. package/dist/columnar-parser-relationships.d.ts.map +1 -1
  12. package/dist/columnar-parser-relationships.js +6 -1
  13. package/dist/columnar-parser-relationships.js.map +1 -1
  14. package/dist/columnar-parser.d.ts +3 -1
  15. package/dist/columnar-parser.d.ts.map +1 -1
  16. package/dist/columnar-parser.js +27 -76
  17. package/dist/columnar-parser.js.map +1 -1
  18. package/dist/compact-entity-index-transport.d.ts +23 -0
  19. package/dist/compact-entity-index-transport.d.ts.map +1 -0
  20. package/dist/compact-entity-index-transport.js +28 -0
  21. package/dist/compact-entity-index-transport.js.map +1 -0
  22. package/dist/compact-entity-index.d.ts +0 -10
  23. package/dist/compact-entity-index.d.ts.map +1 -1
  24. package/dist/compact-entity-index.js +31 -24
  25. package/dist/compact-entity-index.js.map +1 -1
  26. package/dist/data-store-transport.d.ts +2 -12
  27. package/dist/data-store-transport.d.ts.map +1 -1
  28. package/dist/data-store-transport.js +7 -20
  29. package/dist/data-store-transport.js.map +1 -1
  30. package/dist/entity-extractor.d.ts.map +1 -1
  31. package/dist/entity-extractor.js +101 -6
  32. package/dist/entity-extractor.js.map +1 -1
  33. package/dist/entity-refs-from-index.d.ts.map +1 -1
  34. package/dist/entity-refs-from-index.js +12 -2
  35. package/dist/entity-refs-from-index.js.map +1 -1
  36. package/dist/entity-scanner.d.ts +63 -0
  37. package/dist/entity-scanner.d.ts.map +1 -1
  38. package/dist/entity-scanner.js +79 -2
  39. package/dist/entity-scanner.js.map +1 -1
  40. package/dist/express-id.d.ts +47 -0
  41. package/dist/express-id.d.ts.map +1 -0
  42. package/dist/express-id.js +57 -0
  43. package/dist/express-id.js.map +1 -0
  44. package/dist/generated/entities.d.ts +1 -1
  45. package/dist/generated/entities.d.ts.map +1 -1
  46. package/dist/generated/schema-registry.js +146 -146
  47. package/dist/generated/schema-registry.js.map +1 -1
  48. package/dist/georef-epset.d.ts +25 -0
  49. package/dist/georef-epset.d.ts.map +1 -0
  50. package/dist/georef-epset.js +214 -0
  51. package/dist/georef-epset.js.map +1 -0
  52. package/dist/georef-extractor.d.ts +9 -12
  53. package/dist/georef-extractor.d.ts.map +1 -1
  54. package/dist/georef-extractor.js +66 -304
  55. package/dist/georef-extractor.js.map +1 -1
  56. package/dist/georef-map-conversion.d.ts +29 -0
  57. package/dist/georef-map-conversion.d.ts.map +1 -0
  58. package/dist/georef-map-conversion.js +75 -0
  59. package/dist/georef-map-conversion.js.map +1 -0
  60. package/dist/georef-transform.d.ts +25 -0
  61. package/dist/georef-transform.d.ts.map +1 -0
  62. package/dist/georef-transform.js +97 -0
  63. package/dist/georef-transform.js.map +1 -0
  64. package/dist/ifc-schema.d.ts +4 -2
  65. package/dist/ifc-schema.d.ts.map +1 -1
  66. package/dist/ifc-schema.js +15 -5
  67. package/dist/ifc-schema.js.map +1 -1
  68. package/dist/index.d.ts +5 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +5 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/map-unit-label.d.ts +43 -0
  73. package/dist/map-unit-label.d.ts.map +1 -0
  74. package/dist/map-unit-label.js +140 -0
  75. package/dist/map-unit-label.js.map +1 -0
  76. package/dist/material-extractor.d.ts +2 -17
  77. package/dist/material-extractor.d.ts.map +1 -1
  78. package/dist/material-extractor.js +12 -37
  79. package/dist/material-extractor.js.map +1 -1
  80. package/dist/material-layer-reader.d.ts +45 -0
  81. package/dist/material-layer-reader.d.ts.map +1 -0
  82. package/dist/material-layer-reader.js +63 -0
  83. package/dist/material-layer-reader.js.map +1 -0
  84. package/dist/material-resolver.d.ts.map +1 -1
  85. package/dist/material-resolver.js +14 -14
  86. package/dist/material-resolver.js.map +1 -1
  87. package/dist/measure-unit-scale.d.ts +41 -0
  88. package/dist/measure-unit-scale.d.ts.map +1 -0
  89. package/dist/measure-unit-scale.js +53 -0
  90. package/dist/measure-unit-scale.js.map +1 -0
  91. package/dist/on-demand-cache.d.ts +3 -0
  92. package/dist/on-demand-cache.d.ts.map +1 -0
  93. package/dist/on-demand-cache.js +25 -0
  94. package/dist/on-demand-cache.js.map +1 -0
  95. package/dist/on-demand-extractors.d.ts +5 -21
  96. package/dist/on-demand-extractors.d.ts.map +1 -1
  97. package/dist/on-demand-extractors.js +64 -437
  98. package/dist/on-demand-extractors.js.map +1 -1
  99. package/dist/on-demand-georeferencing.d.ts +19 -0
  100. package/dist/on-demand-georeferencing.d.ts.map +1 -0
  101. package/dist/on-demand-georeferencing.js +139 -0
  102. package/dist/on-demand-georeferencing.js.map +1 -0
  103. package/dist/owning-project.d.ts +54 -0
  104. package/dist/owning-project.d.ts.map +1 -0
  105. package/dist/owning-project.js +76 -0
  106. package/dist/owning-project.js.map +1 -0
  107. package/dist/parser.worker.d.ts +14 -0
  108. package/dist/parser.worker.d.ts.map +1 -1
  109. package/dist/parser.worker.js +2 -0
  110. package/dist/parser.worker.js.map +1 -1
  111. package/dist/project-units-symbols.d.ts +33 -0
  112. package/dist/project-units-symbols.d.ts.map +1 -0
  113. package/dist/project-units-symbols.js +265 -0
  114. package/dist/project-units-symbols.js.map +1 -0
  115. package/dist/project-units.d.ts +11 -19
  116. package/dist/project-units.d.ts.map +1 -1
  117. package/dist/project-units.js +9 -267
  118. package/dist/project-units.js.map +1 -1
  119. package/dist/property-set-merge.d.ts +65 -8
  120. package/dist/property-set-merge.d.ts.map +1 -1
  121. package/dist/property-set-merge.js +130 -29
  122. package/dist/property-set-merge.js.map +1 -1
  123. package/dist/property-value-parser.d.ts +55 -0
  124. package/dist/property-value-parser.d.ts.map +1 -0
  125. package/dist/property-value-parser.js +277 -0
  126. package/dist/property-value-parser.js.map +1 -0
  127. package/dist/quantity-collect.d.ts +135 -0
  128. package/dist/quantity-collect.d.ts.map +1 -0
  129. package/dist/quantity-collect.js +253 -0
  130. package/dist/quantity-collect.js.map +1 -0
  131. package/dist/query-backend-maps.d.ts +5 -8
  132. package/dist/query-backend-maps.d.ts.map +1 -1
  133. package/dist/query-backend-maps.js +159 -15
  134. package/dist/query-backend-maps.js.map +1 -1
  135. package/dist/relationship-extractor.d.ts.map +1 -1
  136. package/dist/relationship-extractor.js +18 -0
  137. package/dist/relationship-extractor.js.map +1 -1
  138. package/dist/scan-entities-balanced.d.ts +38 -0
  139. package/dist/scan-entities-balanced.d.ts.map +1 -0
  140. package/dist/scan-entities-balanced.js +258 -0
  141. package/dist/scan-entities-balanced.js.map +1 -0
  142. package/dist/scan-worker-inline.d.ts +17 -11
  143. package/dist/scan-worker-inline.d.ts.map +1 -1
  144. package/dist/scan-worker-inline.js +9 -235
  145. package/dist/scan-worker-inline.js.map +1 -1
  146. package/dist/scan-worker-source.d.ts +9 -0
  147. package/dist/scan-worker-source.d.ts.map +1 -0
  148. package/dist/scan-worker-source.js +383 -0
  149. package/dist/scan-worker-source.js.map +1 -0
  150. package/dist/source-header.d.ts +23 -1
  151. package/dist/source-header.d.ts.map +1 -1
  152. package/dist/source-header.js +133 -43
  153. package/dist/source-header.js.map +1 -1
  154. package/dist/spatial-hierarchy-builder.js +3 -3
  155. package/dist/spatial-hierarchy-builder.js.map +1 -1
  156. package/dist/step-lexing.d.ts +94 -0
  157. package/dist/step-lexing.d.ts.map +1 -1
  158. package/dist/step-lexing.js +292 -25
  159. package/dist/step-lexing.js.map +1 -1
  160. package/dist/step-trivia.d.ts +60 -0
  161. package/dist/step-trivia.d.ts.map +1 -0
  162. package/dist/step-trivia.js +63 -0
  163. package/dist/step-trivia.js.map +1 -0
  164. package/dist/tokenizer.d.ts +20 -21
  165. package/dist/tokenizer.d.ts.map +1 -1
  166. package/dist/tokenizer.js +164 -203
  167. package/dist/tokenizer.js.map +1 -1
  168. package/dist/unit-extractor.d.ts +56 -1
  169. package/dist/unit-extractor.d.ts.map +1 -1
  170. package/dist/unit-extractor.js +80 -6
  171. package/dist/unit-extractor.js.map +1 -1
  172. package/dist/worker-parser.d.ts +11 -1
  173. package/dist/worker-parser.d.ts.map +1 -1
  174. package/dist/worker-parser.js +16 -2
  175. package/dist/worker-parser.js.map +1 -1
  176. package/package.json +6 -6
@@ -1,10 +1,17 @@
1
1
  /* This Source Code Form is subject to the terms of the Mozilla Public
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * On-demand extraction functions for classifications, materials, documents,
6
+ * georeferencing, relationships, and type properties.
7
+ *
8
+ * These functions parse data lazily from the IFC source buffer when accessed,
9
+ * rather than pre-building all data upfront during initial parse.
10
+ */
4
11
  import { EntityExtractor } from './entity-extractor.js';
5
- import { RelationshipType, PropertyValueType, QuantityType, } from '@ifc-lite/data';
6
- import { QUANTITY_TYPE_MAP } from './columnar-parser-indexes.js';
7
- import { extractGeoreferencing as extractGeorefFromEntities } from './georef-extractor.js';
12
+ import { RelationshipType, } from '@ifc-lite/data';
13
+ import { readQuantitySet } from './quantity-collect.js';
14
+ import { appendSetsFromSecondSource, setIdentityKey } from './property-set-merge.js';
8
15
  // Re-export classification and material resolvers
9
16
  export { extractClassificationsOnDemand, extractClassificationSystemsOnDemand } from './classification-resolver.js';
10
17
  export { extractMaterialsOnDemand, extractAllMaterialsOnDemand } from './material-resolver.js';
@@ -13,219 +20,11 @@ import { resolveAllMaterialDefIds as resolveAllMaterialDefIdsImpl, collectMateri
13
20
  // ============================================================================
14
21
  // Property Value Parsing Helpers
15
22
  // ============================================================================
16
- /**
17
- * Parse a property entity's value based on its IFC type.
18
- * Handles all 6 IfcProperty subtypes:
19
- * - IfcPropertySingleValue: direct value
20
- * - IfcPropertyEnumeratedValue: list of enum values → joined string
21
- * - IfcPropertyBoundedValue: upper/lower bounds → "value [min – max]"
22
- * - IfcPropertyListValue: list of values → joined string
23
- * - IfcPropertyTableValue: defining/defined value pairs → "Table(N rows)"
24
- * - IfcPropertyReferenceValue: entity reference → "Reference #ID"
25
- */
26
- export function parsePropertyValue(propEntity) {
27
- const attrs = propEntity.attributes || [];
28
- const typeUpper = propEntity.type.toUpperCase();
29
- switch (typeUpper) {
30
- case 'IFCPROPERTYENUMERATEDVALUE': {
31
- // [Name, Description, EnumerationValues (list), EnumerationReference]
32
- const enumValues = attrs[2];
33
- if (Array.isArray(enumValues)) {
34
- const values = enumValues.map(v => {
35
- if (Array.isArray(v) && v.length === 2)
36
- return String(v[1]); // Typed value
37
- return String(v);
38
- }).filter(v => v !== 'null' && v !== 'undefined');
39
- // Surface the raw value list separately so IDS facet
40
- // checks can iterate "any matching value passes". The
41
- // joined display string remains the primary `value`
42
- // for visualisation/property-table consumers.
43
- return { type: 0, value: values.join(', ') || null, values };
44
- }
45
- return { type: 0, value: null };
46
- }
47
- case 'IFCPROPERTYBOUNDEDVALUE': {
48
- // [Name, Description, UpperBoundValue, LowerBoundValue, Unit, SetPointValue]
49
- const upper = extractNumericValue(attrs[2]);
50
- const lower = extractNumericValue(attrs[3]);
51
- const setPoint = extractNumericValue(attrs[5]);
52
- const displayValue = setPoint ?? upper ?? lower;
53
- let display = displayValue != null ? String(displayValue) : '';
54
- if (lower != null && upper != null) {
55
- display += ` [${lower} – ${upper}]`;
56
- }
57
- // Surface every defined bound as a candidate value — IDS
58
- // bounded-property checks pass when ANY of the bounds /
59
- // setpoint matches the constraint, per upstream ifctester.
60
- const candidates = [];
61
- if (lower != null)
62
- candidates.push(String(lower));
63
- if (upper != null && upper !== lower)
64
- candidates.push(String(upper));
65
- if (setPoint != null && setPoint !== lower && setPoint !== upper) {
66
- candidates.push(String(setPoint));
67
- }
68
- // Carry the IFC-declared measure tag so the IDS-side data
69
- // type comparison and unit conversion both work.
70
- const inferDataType = (attr) => {
71
- if (Array.isArray(attr) && attr.length === 2) {
72
- return String(attr[0]).toUpperCase();
73
- }
74
- return undefined;
75
- };
76
- const dataType = inferDataType(attrs[5]) ||
77
- inferDataType(attrs[2]) ||
78
- inferDataType(attrs[3]);
79
- return {
80
- type: displayValue != null ? 1 : 0,
81
- value: display || null,
82
- ...(candidates.length > 0 ? { values: candidates } : {}),
83
- ...(dataType ? { dataType } : {}),
84
- };
85
- }
86
- case 'IFCPROPERTYLISTVALUE': {
87
- // [Name, Description, ListValues (list), Unit]
88
- const listValues = attrs[2];
89
- if (Array.isArray(listValues)) {
90
- const values = listValues.map(v => {
91
- if (Array.isArray(v) && v.length === 2)
92
- return String(v[1]);
93
- return String(v);
94
- }).filter(v => v !== 'null' && v !== 'undefined');
95
- return { type: 0, value: values.join(', ') || null, values };
96
- }
97
- return { type: 0, value: null };
98
- }
99
- case 'IFCPROPERTYTABLEVALUE': {
100
- // [Name, Description, DefiningValues, DefinedValues, ...]
101
- const definingValues = attrs[2];
102
- const definedValues = attrs[3];
103
- const rowCount = Array.isArray(definingValues) ? definingValues.length : 0;
104
- if (rowCount > 0 && Array.isArray(definedValues) && Array.isArray(definingValues)) {
105
- // Surface both defining and defined values as candidate
106
- // matches — IDS table-value checks pass when ANY entry
107
- // matches the constraint (per upstream ifctester).
108
- const stringify = (v) => {
109
- if (Array.isArray(v) && v.length === 2)
110
- return String(v[1]);
111
- return String(v);
112
- };
113
- const values = [
114
- ...definingValues.map(stringify),
115
- ...definedValues.map(stringify),
116
- ].filter(v => v !== 'null' && v !== 'undefined');
117
- // Tables mix types per column (label / length / …),
118
- // so we can't surface a single representative
119
- // dataType. Leaving it unset lets the IDS check fall
120
- // through to a pure value match against any of the
121
- // candidates — which is what upstream ifctester does
122
- // for table values.
123
- return {
124
- type: 0,
125
- value: `Table (${rowCount} rows)`,
126
- values,
127
- };
128
- }
129
- return { type: 0, value: null };
130
- }
131
- case 'IFCPROPERTYREFERENCEVALUE': {
132
- // [Name, Description, PropertyReference]
133
- const refValue = attrs[2];
134
- if (typeof refValue === 'number') {
135
- return { type: 0, value: `#${refValue}` };
136
- }
137
- return { type: 0, value: null };
138
- }
139
- default: {
140
- // IfcPropertySingleValue and fallback: [Name, Description, NominalValue, Unit]
141
- const nominalValue = attrs[2];
142
- let type = PropertyValueType.String;
143
- let value = nominalValue;
144
- let dataType;
145
- // Handle typed values like IFCBOOLEAN(.T.), IFCREAL(1.5)
146
- if (Array.isArray(nominalValue) && nominalValue.length === 2) {
147
- const innerValue = nominalValue[1];
148
- const typeName = String(nominalValue[0]).toUpperCase();
149
- dataType = typeName;
150
- if (typeName.includes('BOOLEAN')) {
151
- type = PropertyValueType.Boolean;
152
- value = innerValue === '.T.' || innerValue === true;
153
- }
154
- else if (typeName.includes('LOGICAL')) {
155
- type = PropertyValueType.Logical;
156
- // Preserve .U. (unknown) as null; .T./.F. as boolean
157
- if (innerValue === '.U.' || innerValue === '.X.') {
158
- value = null;
159
- }
160
- else {
161
- value = innerValue === '.T.' || innerValue === true;
162
- }
163
- }
164
- else if (typeof innerValue === 'number') {
165
- // Preserve the IFC-declared numeric measure (IFCREAL,
166
- // IFCINTEGER, IFCLENGTHMEASURE, IFCAREAMEASURE, …) —
167
- // the source explicitly tagged the value, so don't
168
- // re-infer from JS number-ness (which would
169
- // misclassify e.g. `IFCREAL(0.0)` as integer).
170
- if (typeName === 'IFCINTEGER' || typeName === 'IFCCOUNTMEASURE') {
171
- type = PropertyValueType.Integer;
172
- }
173
- else if (typeName === 'IFCREAL' ||
174
- typeName.endsWith('MEASURE') ||
175
- typeName.endsWith('RATIO')) {
176
- type = PropertyValueType.Real;
177
- }
178
- else if (Number.isInteger(innerValue)) {
179
- type = PropertyValueType.Integer;
180
- }
181
- else {
182
- type = PropertyValueType.Real;
183
- }
184
- value = innerValue;
185
- }
186
- else {
187
- type = PropertyValueType.String;
188
- value = String(innerValue);
189
- }
190
- }
191
- else if (typeof nominalValue === 'number') {
192
- type = Number.isInteger(nominalValue) ? PropertyValueType.Integer : PropertyValueType.Real;
193
- }
194
- else if (typeof nominalValue === 'boolean') {
195
- type = PropertyValueType.Boolean;
196
- }
197
- else if (nominalValue !== null && nominalValue !== undefined) {
198
- // Normalize untagged STEP enumeration tokens. Conformant IFC wraps
199
- // booleans as IFCBOOLEAN(.T.) (handled above), but some authoring
200
- // tools emit the bare tokens directly in the NominalValue slot.
201
- if (nominalValue === '.T.') {
202
- type = PropertyValueType.Boolean;
203
- value = true;
204
- }
205
- else if (nominalValue === '.F.') {
206
- type = PropertyValueType.Boolean;
207
- value = false;
208
- }
209
- else if (nominalValue === '.U.' || nominalValue === '.X.') {
210
- type = PropertyValueType.Logical;
211
- value = null;
212
- }
213
- else {
214
- value = String(nominalValue);
215
- }
216
- }
217
- return { type, value, ...(dataType ? { dataType } : {}) };
218
- }
219
- }
220
- }
221
- /** Extract a numeric value from a possibly typed STEP value. */
222
- export function extractNumericValue(attr) {
223
- if (typeof attr === 'number')
224
- return attr;
225
- if (Array.isArray(attr) && attr.length === 2 && typeof attr[1] === 'number')
226
- return attr[1];
227
- return null;
228
- }
23
+ //
24
+ // Moved to ./property-value-parser.ts to stay under the module-size budget;
25
+ // re-exported here so every existing import of this module keeps resolving.
26
+ export { parsePropertyValue, extractNumericValue, resolveComplexPropertyValue, parsePropertyValueWithComplex, } from './property-value-parser.js';
27
+ import { parsePropertyValueWithComplex } from './property-value-parser.js';
229
28
  // ============================================================================
230
29
  // Property Set Extraction Helpers
231
30
  // ============================================================================
@@ -247,7 +46,7 @@ export function extractPsetsFromIds(store, extractor, psetIds) {
247
46
  continue;
248
47
  const psetAttrs = psetEntity.attributes || [];
249
48
  const psetGlobalId = typeof psetAttrs[0] === 'string' ? psetAttrs[0] : undefined;
250
- const psetName = typeof psetAttrs[2] === 'string' ? psetAttrs[2] : `PropertySet #${psetId}`;
49
+ const psetName = typeof psetAttrs[2] === 'string' ? psetAttrs[2] : ''; // not `PropertySet #<id>` (#3530)
251
50
  const hasProperties = psetAttrs[4];
252
51
  const properties = [];
253
52
  if (Array.isArray(hasProperties)) {
@@ -264,7 +63,7 @@ export function extractPsetsFromIds(store, extractor, psetIds) {
264
63
  const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
265
64
  if (!propName)
266
65
  continue;
267
- const parsed = parsePropertyValue(propEntity);
66
+ const parsed = parsePropertyValueWithComplex(store, extractor, propEntity);
268
67
  const entry = {
269
68
  name: propName,
270
69
  type: parsed.type,
@@ -279,8 +78,9 @@ export function extractPsetsFromIds(store, extractor, psetIds) {
279
78
  }
280
79
  // Only surface sets that actually carry properties. An empty, named set
281
80
  // contributes nothing and — because extractTypePropertiesOnDemand dedups
282
- // by name — an empty set from one source could otherwise suppress a
283
- // populated same-named set from another (symmetric with extractQsetsFromIds).
81
+ // by (name, globalId) identity — an empty set from one source could
82
+ // otherwise suppress a populated same-identity set from another
83
+ // (symmetric with extractQsetsFromIds).
284
84
  if (properties.length > 0) {
285
85
  result.push({ name: psetName, globalId: psetGlobalId, properties });
286
86
  }
@@ -317,31 +117,26 @@ export function extractTypePropertiesOnDemand(store, entityId) {
317
117
  ? typeEntity.attributes[2]
318
118
  : typeRef.type;
319
119
  const allPsets = [];
320
- const seenPsetNames = new Set();
120
+ const seenPsetKeys = new Set();
121
+ const ownPsetIds = new Set();
321
122
  // Source 1: HasPropertySets attribute on the type entity (index 5 for IfcTypeObject subtypes)
322
123
  // Works for both IFC2X3 and IFC4
323
124
  if (typeEntity) {
324
125
  const hasPropertySets = typeEntity.attributes?.[5];
325
126
  if (Array.isArray(hasPropertySets)) {
326
- const psetIds = hasPropertySets.filter((id) => typeof id === 'number');
327
- const psets = extractPsetsFromIds(store, extractor, psetIds);
328
- for (const pset of psets) {
329
- seenPsetNames.add(pset.name);
127
+ for (const id of hasPropertySets)
128
+ if (typeof id === 'number')
129
+ ownPsetIds.add(id);
130
+ for (const pset of extractPsetsFromIds(store, extractor, [...ownPsetIds])) {
131
+ seenPsetKeys.add(setIdentityKey(pset));
330
132
  allPsets.push(pset);
331
133
  }
332
134
  }
333
135
  }
334
136
  // Source 2: onDemandPropertyMap for the type entity (IFC4: via IFCRELDEFINESBYPROPERTIES)
335
- if (store.onDemandPropertyMap) {
336
- const typePsetIds = store.onDemandPropertyMap.get(typeId);
337
- if (typePsetIds && typePsetIds.length > 0) {
338
- const psets = extractPsetsFromIds(store, extractor, typePsetIds);
339
- for (const pset of psets) {
340
- if (!seenPsetNames.has(pset.name)) {
341
- allPsets.push(pset);
342
- }
343
- }
344
- }
137
+ const typePsetIds = store.onDemandPropertyMap?.get(typeId);
138
+ if (typePsetIds && typePsetIds.length > 0) {
139
+ appendSetsFromSecondSource(allPsets, ownPsetIds, seenPsetKeys, typePsetIds, (ids) => extractPsetsFromIds(store, extractor, ids));
345
140
  }
346
141
  if (allPsets.length === 0)
347
142
  return null;
@@ -365,28 +160,23 @@ export function extractTypeEntityOwnProperties(store, typeEntityId) {
365
160
  if (!typeEntity)
366
161
  return [];
367
162
  const allPsets = [];
368
- const seenPsetNames = new Set();
163
+ const seenPsetKeys = new Set();
164
+ const ownPsetIds = new Set();
369
165
  // Source 1: HasPropertySets attribute (index 5 for IfcTypeObject subtypes)
370
166
  const hasPropertySets = typeEntity.attributes?.[5];
371
167
  if (Array.isArray(hasPropertySets)) {
372
- const psetIds = hasPropertySets.filter((id) => typeof id === 'number');
373
- const psets = extractPsetsFromIds(store, extractor, psetIds);
374
- for (const pset of psets) {
375
- seenPsetNames.add(pset.name);
168
+ for (const id of hasPropertySets)
169
+ if (typeof id === 'number')
170
+ ownPsetIds.add(id);
171
+ for (const pset of extractPsetsFromIds(store, extractor, [...ownPsetIds])) {
172
+ seenPsetKeys.add(setIdentityKey(pset));
376
173
  allPsets.push(pset);
377
174
  }
378
175
  }
379
176
  // Source 2: onDemandPropertyMap (IFC4: via IFCRELDEFINESBYPROPERTIES)
380
- if (store.onDemandPropertyMap) {
381
- const typePsetIds = store.onDemandPropertyMap.get(typeEntityId);
382
- if (typePsetIds && typePsetIds.length > 0) {
383
- const psets = extractPsetsFromIds(store, extractor, typePsetIds);
384
- for (const pset of psets) {
385
- if (!seenPsetNames.has(pset.name)) {
386
- allPsets.push(pset);
387
- }
388
- }
389
- }
177
+ const typePsetIds = store.onDemandPropertyMap?.get(typeEntityId);
178
+ if (typePsetIds && typePsetIds.length > 0) {
179
+ appendSetsFromSecondSource(allPsets, ownPsetIds, seenPsetKeys, typePsetIds, (ids) => extractPsetsFromIds(store, extractor, ids));
390
180
  }
391
181
  return allPsets;
392
182
  }
@@ -408,40 +198,14 @@ export function extractQsetsFromIds(store, extractor, qsetIds) {
408
198
  // Only extract IFCELEMENTQUANTITY entities (skip property sets etc.)
409
199
  if (qsetRef.type.toUpperCase() !== 'IFCELEMENTQUANTITY')
410
200
  continue;
411
- const qsetEntity = extractor.extractEntity(qsetRef);
412
- if (!qsetEntity)
413
- continue;
414
- const qsetAttrs = qsetEntity.attributes || [];
415
- const qsetName = typeof qsetAttrs[2] === 'string' ? qsetAttrs[2] : `QuantitySet #${qsetId}`;
416
- const hasQuantities = qsetAttrs[5];
417
- const quantities = [];
418
- if (Array.isArray(hasQuantities)) {
419
- for (const qtyRef of hasQuantities) {
420
- if (typeof qtyRef !== 'number')
421
- continue;
422
- const qtyEntityRef = store.entityIndex.byId.get(qtyRef);
423
- if (!qtyEntityRef)
424
- continue;
425
- const qtyEntity = extractor.extractEntity(qtyEntityRef);
426
- if (!qtyEntity)
427
- continue;
428
- const qtyAttrs = qtyEntity.attributes || [];
429
- const qtyName = typeof qtyAttrs[0] === 'string' ? qtyAttrs[0] : '';
430
- if (!qtyName)
431
- continue;
432
- const qtyType = QUANTITY_TYPE_MAP[qtyEntity.type.toUpperCase()] ?? QuantityType.Count;
433
- // Value is at index 3 for the simple IfcQuantity* subtypes.
434
- const value = typeof qtyAttrs[3] === 'number' ? qtyAttrs[3] : 0;
435
- quantities.push({ name: qtyName, type: qtyType, value });
436
- }
437
- }
438
- // Only surface sets that actually carry quantities. An empty set would
439
- // add nothing to a schedule, and (because `extractTypeQuantitiesOnDemand`
440
- // dedups by name) an empty set from one source could otherwise suppress a
441
- // populated same-named set from another.
442
- if (quantities.length > 0) {
443
- result.push({ name: qsetName, quantities });
444
- }
201
+ // A set that walks to zero quantities is dropped — see
202
+ // {@link readQuantitySet}. Here that also stops an empty set from one
203
+ // source suppressing a populated same-identity set from another, since
204
+ // `extractTypeQuantitiesOnDemand` still dedups NAMED sets by
205
+ // `(name, globalId)` identity (see {@link appendSetsFromSecondSource}).
206
+ const qset = readQuantitySet(store, extractor, qsetRef);
207
+ if (qset)
208
+ result.push(qset);
445
209
  }
446
210
  return result;
447
211
  }
@@ -474,28 +238,26 @@ export function extractTypeQuantitiesOnDemand(store, entityId) {
474
238
  ? typeEntity.attributes[2]
475
239
  : typeRef.type;
476
240
  const allQsets = [];
477
- const seenQsetNames = new Set();
241
+ const seenQsetKeys = new Set();
242
+ const ownSetIds = new Set();
478
243
  // Source 1: HasPropertySets attribute on the type (index 5) — quantity sets
479
244
  // live alongside property sets in this IfcPropertySetDefinition list.
480
245
  if (typeEntity) {
481
246
  const hasPropertySets = typeEntity.attributes?.[5];
482
247
  if (Array.isArray(hasPropertySets)) {
483
- const ids = hasPropertySets.filter((id) => typeof id === 'number');
484
- for (const qset of extractQsetsFromIds(store, extractor, ids)) {
485
- seenQsetNames.add(qset.name);
248
+ for (const id of hasPropertySets)
249
+ if (typeof id === 'number')
250
+ ownSetIds.add(id);
251
+ for (const qset of extractQsetsFromIds(store, extractor, [...ownSetIds])) {
252
+ seenQsetKeys.add(setIdentityKey(qset));
486
253
  allQsets.push(qset);
487
254
  }
488
255
  }
489
256
  }
490
257
  // Source 2: onDemandQuantityMap for the type entity (IFC4 IfcRelDefinesByProperties).
491
- if (store.onDemandQuantityMap) {
492
- const typeQsetIds = store.onDemandQuantityMap.get(typeId);
493
- if (typeQsetIds && typeQsetIds.length > 0) {
494
- for (const qset of extractQsetsFromIds(store, extractor, typeQsetIds)) {
495
- if (!seenQsetNames.has(qset.name))
496
- allQsets.push(qset);
497
- }
498
- }
258
+ const typeQsetIds = store.onDemandQuantityMap?.get(typeId);
259
+ if (typeQsetIds && typeQsetIds.length > 0) {
260
+ appendSetsFromSecondSource(allQsets, ownSetIds, seenQsetKeys, typeQsetIds, (ids) => extractQsetsFromIds(store, extractor, ids));
499
261
  }
500
262
  if (allQsets.length === 0)
501
263
  return null;
@@ -705,146 +467,11 @@ export function extractGroupMembersOnDemand(store, groupId) {
705
467
  // ============================================================================
706
468
  // On-Demand Georeferencing Extraction
707
469
  // ============================================================================
708
- /**
709
- * Extract georeferencing info from on-demand store (source buffer + entityIndex).
710
- * Bridges to the entity-based georef extractor by resolving entities lazily.
711
- *
712
- * Memoized per store. On models without an IfcMapConversion (e.g. IFC2x3 files
713
- * that carry CRS in ePSet_MapConversion / ePSet_ProjectedCRS) the underlying
714
- * scan decodes EVERY IfcPropertySet from the source buffer to match by name —
715
- * tens of thousands of decodes on property-heavy models. The viewer calls this
716
- * on the load/render path (ViewportContainer's Cesium-availability check), which
717
- * re-runs on every streamed geometry batch, so without caching the cost is
718
- * O(batches x propertySets) and can turn a multi-second load into minutes.
719
- * Caching collapses it to a single scan per store. Safe because the result is a
720
- * pure function of the immutable source + entityIndex; georef *edits* are layered
721
- * on top later in getEffectiveGeoreference(), not here.
722
- */
723
- /**
724
- * Memoize an O(entities) on-demand extraction per store. On-demand extractors
725
- * derive purely from the immutable source + entityIndex, but the viewer calls
726
- * them on render/stream hot paths where they can re-run once per geometry batch
727
- * (regression #1404). Caching by store collapses that to one scan per model.
728
- * Use this for any new `extract*OnDemand` so the whole family stays O(1)-per-call
729
- * regardless of how often the render layer invokes it.
730
- */
731
- const onDemandCaches = new WeakMap();
732
- function oncePerStore(store, key, compute) {
733
- let byKey = onDemandCaches.get(store);
734
- if (!byKey) {
735
- byKey = new Map();
736
- onDemandCaches.set(store, byKey);
737
- }
738
- if (byKey.has(key))
739
- return byKey.get(key);
740
- const value = compute();
741
- byKey.set(key, value);
742
- return value;
743
- }
744
- export function extractGeoreferencingOnDemand(store) {
745
- // Don't cache a not-yet-loaded store — it may gain source/entityIndex later.
746
- if (!store.source?.length || !store.entityIndex)
747
- return null;
748
- return oncePerStore(store, 'georef', () => computeGeoreferencingOnDemand(store));
749
- }
750
- function computeGeoreferencingOnDemand(store) {
751
- if (!store.source?.length || !store.entityIndex)
752
- return null;
753
- const extractor = new EntityExtractor(store.source);
754
- const { byId, byType } = store.entityIndex;
755
- // Build a lightweight entity map for just the georef-related types
756
- const entityMap = new Map();
757
- const typeMap = new Map();
758
- for (const typeName of ['IFCMAPCONVERSION', 'IFCPROJECTEDCRS', 'IFCSITE']) {
759
- const ids = byType.get(typeName);
760
- if (!ids?.length)
761
- continue;
762
- // Use mixed-case for the georef extractor's type lookup
763
- const displayName = typeName === 'IFCMAPCONVERSION'
764
- ? 'IfcMapConversion'
765
- : typeName === 'IFCPROJECTEDCRS'
766
- ? 'IfcProjectedCRS'
767
- : 'IfcSite';
768
- typeMap.set(displayName, ids);
769
- for (const id of ids) {
770
- const ref = byId.get(id);
771
- if (!ref)
772
- continue;
773
- const entity = extractor.extractEntity(ref);
774
- if (entity) {
775
- entityMap.set(id, entity);
776
- // For IfcProjectedCRS, also resolve the MapUnit reference (attribute [6])
777
- // so the georef extractor can determine the actual unit scale
778
- if (typeName === 'IFCPROJECTEDCRS' && entity.attributes) {
779
- const mapUnitAttr = entity.attributes[6];
780
- const mapUnitRefId = typeof mapUnitAttr === 'number' ? mapUnitAttr : null;
781
- if (mapUnitRefId && !entityMap.has(mapUnitRefId)) {
782
- const unitRef = byId.get(mapUnitRefId);
783
- if (unitRef) {
784
- const unitEntity = extractor.extractEntity(unitRef);
785
- if (unitEntity)
786
- entityMap.set(mapUnitRefId, unitEntity);
787
- }
788
- }
789
- }
790
- }
791
- }
792
- }
793
- // IFC2x3 fallback: models without IfcMapConversion store georeferencing in
794
- // ePSet_MapConversion / ePSet_ProjectedCRS property sets. Those aren't
795
- // loaded above, so the ePSet path in extractGeorefFromEntities had nothing
796
- // to read and the model fell back to the legacy IfcSite EPSG:4326 (wrong
797
- // CRS). Only scan property sets when no IfcMapConversion exists, and only
798
- // pull in the georef ePSets + their values — not every pset in the model.
799
- if (!typeMap.has('IfcMapConversion')) {
800
- const psetIds = byType.get('IFCPROPERTYSET');
801
- if (psetIds?.length) {
802
- const georefPsetIds = [];
803
- const childIds = new Set();
804
- for (const id of psetIds) {
805
- const ref = byId.get(id);
806
- if (!ref)
807
- continue;
808
- const entity = extractor.extractEntity(ref);
809
- if (!entity?.attributes)
810
- continue;
811
- // IfcPropertySet: Name (2), HasProperties (4)
812
- const name = typeof entity.attributes[2] === 'string'
813
- ? entity.attributes[2].toLowerCase()
814
- : '';
815
- if (name !== 'epset_mapconversion' && name !== 'epset_projectedcrs')
816
- continue;
817
- entityMap.set(id, entity);
818
- georefPsetIds.push(id);
819
- const props = entity.attributes[4];
820
- if (Array.isArray(props)) {
821
- for (const propRef of props) {
822
- const propId = typeof propRef === 'number' ? propRef : null;
823
- if (propId === null || childIds.has(propId))
824
- continue;
825
- // Property atoms may be deferred on huge files (not in
826
- // the primary byId index) — fall back like refFromStore.
827
- const childRef = byId.get(propId) ?? store.deferredEntityIndex?.get(propId);
828
- if (!childRef)
829
- continue;
830
- const child = extractor.extractEntity(childRef);
831
- if (child) {
832
- entityMap.set(propId, child);
833
- childIds.add(propId);
834
- }
835
- }
836
- }
837
- }
838
- if (georefPsetIds.length) {
839
- typeMap.set('IfcPropertySet', georefPsetIds);
840
- }
841
- }
842
- }
843
- if (entityMap.size === 0)
844
- return null;
845
- // Cast to IfcEntity (they share the same shape)
846
- return extractGeorefFromEntities(entityMap, typeMap);
847
- }
470
+ // The per-store memo now lives in ./on-demand-cache.ts (import it there for
471
+ // any new extract*OnDemand) and the georeferencing extractor in
472
+ // ./on-demand-georeferencing.ts, re-exported here so every existing import
473
+ // of this module keeps resolving.
474
+ export { extractGeoreferencingOnDemand } from './on-demand-georeferencing.js';
848
475
  const materialPropertyIndexCache = new WeakMap();
849
476
  /** Resolve an entity ref from the primary index, falling back to deferred atoms. */
850
477
  function refFromStore(store, id) {
@@ -926,7 +553,7 @@ function getMaterialPropertyIndex(store) {
926
553
  const propName = typeof propAttrs[0] === 'string' ? propAttrs[0] : '';
927
554
  if (!propName)
928
555
  continue;
929
- const pv = parsePropertyValue(propEntity);
556
+ const pv = parsePropertyValueWithComplex(store, extractor, propEntity);
930
557
  const entry = {
931
558
  name: propName,
932
559
  type: pv.type,