@ifc-lite/parser 9.1.0 → 9.5.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 (201) hide show
  1. package/README.md +35 -0
  2. package/dist/abort-error.d.ts +7 -0
  3. package/dist/abort-error.d.ts.map +1 -0
  4. package/dist/abort-error.js +17 -0
  5. package/dist/abort-error.js.map +1 -0
  6. package/dist/classification-overlay.d.ts +13 -0
  7. package/dist/classification-overlay.d.ts.map +1 -0
  8. package/dist/classification-overlay.js +65 -0
  9. package/dist/classification-overlay.js.map +1 -0
  10. package/dist/classification-resolver.d.ts +13 -9
  11. package/dist/classification-resolver.d.ts.map +1 -1
  12. package/dist/classification-resolver.js +109 -74
  13. package/dist/classification-resolver.js.map +1 -1
  14. package/dist/columnar-parser-attributes.d.ts +1 -1
  15. package/dist/columnar-parser-attributes.d.ts.map +1 -1
  16. package/dist/columnar-parser-attributes.js +16 -6
  17. package/dist/columnar-parser-attributes.js.map +1 -1
  18. package/dist/columnar-parser-root-attributes.d.ts +15 -1
  19. package/dist/columnar-parser-root-attributes.d.ts.map +1 -1
  20. package/dist/columnar-parser-root-attributes.js +26 -11
  21. package/dist/columnar-parser-root-attributes.js.map +1 -1
  22. package/dist/columnar-parser.d.ts +13 -15
  23. package/dist/columnar-parser.d.ts.map +1 -1
  24. package/dist/columnar-parser.js +11 -23
  25. package/dist/columnar-parser.js.map +1 -1
  26. package/dist/current-occurrence-quantities.d.ts +13 -0
  27. package/dist/current-occurrence-quantities.d.ts.map +1 -0
  28. package/dist/current-occurrence-quantities.js +69 -0
  29. package/dist/current-occurrence-quantities.js.map +1 -0
  30. package/dist/current-project-units.d.ts +11 -0
  31. package/dist/current-project-units.d.ts.map +1 -0
  32. package/dist/current-project-units.js +41 -0
  33. package/dist/current-project-units.js.map +1 -0
  34. package/dist/current-type-quantities.d.ts +13 -0
  35. package/dist/current-type-quantities.d.ts.map +1 -0
  36. package/dist/current-type-quantities.js +274 -0
  37. package/dist/current-type-quantities.js.map +1 -0
  38. package/dist/data-store-transport.d.ts +1 -0
  39. package/dist/data-store-transport.d.ts.map +1 -1
  40. package/dist/data-store-transport.js +4 -4
  41. package/dist/data-store-transport.js.map +1 -1
  42. package/dist/document-resolver.d.ts +23 -0
  43. package/dist/document-resolver.d.ts.map +1 -0
  44. package/dist/document-resolver.js +122 -0
  45. package/dist/document-resolver.js.map +1 -0
  46. package/dist/effective-entity-record.d.ts +3 -2
  47. package/dist/effective-entity-record.d.ts.map +1 -1
  48. package/dist/effective-entity-record.js +5 -5
  49. package/dist/effective-entity-record.js.map +1 -1
  50. package/dist/effective-metadata-record.d.ts +23 -0
  51. package/dist/effective-metadata-record.d.ts.map +1 -0
  52. package/dist/effective-metadata-record.js +50 -0
  53. package/dist/effective-metadata-record.js.map +1 -0
  54. package/dist/effective-storey.d.ts.map +1 -1
  55. package/dist/effective-storey.js +5 -0
  56. package/dist/effective-storey.js.map +1 -1
  57. package/dist/entity-scanner.d.ts +6 -8
  58. package/dist/entity-scanner.d.ts.map +1 -1
  59. package/dist/entity-scanner.js +26 -13
  60. package/dist/entity-scanner.js.map +1 -1
  61. package/dist/georef-extractor.d.ts +1 -1
  62. package/dist/georef-extractor.d.ts.map +1 -1
  63. package/dist/georef-extractor.js +1 -1
  64. package/dist/georef-extractor.js.map +1 -1
  65. package/dist/ifc-schema.d.ts +2 -1
  66. package/dist/ifc-schema.d.ts.map +1 -1
  67. package/dist/ifc-schema.js +4 -3
  68. package/dist/ifc-schema.js.map +1 -1
  69. package/dist/index.d.ts +9 -12
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +9 -12
  72. package/dist/index.js.map +1 -1
  73. package/dist/length-unit-source.d.ts +25 -0
  74. package/dist/length-unit-source.d.ts.map +1 -0
  75. package/dist/length-unit-source.js +14 -0
  76. package/dist/length-unit-source.js.map +1 -0
  77. package/dist/material-definitions.d.ts +10 -0
  78. package/dist/material-definitions.d.ts.map +1 -0
  79. package/dist/material-definitions.js +224 -0
  80. package/dist/material-definitions.js.map +1 -0
  81. package/dist/material-overlay.d.ts +38 -0
  82. package/dist/material-overlay.d.ts.map +1 -0
  83. package/dist/material-overlay.js +146 -0
  84. package/dist/material-overlay.js.map +1 -0
  85. package/dist/material-properties-extractor.d.ts +2 -11
  86. package/dist/material-properties-extractor.d.ts.map +1 -1
  87. package/dist/material-properties-extractor.js +21 -38
  88. package/dist/material-properties-extractor.js.map +1 -1
  89. package/dist/material-resolver.d.ts +6 -4
  90. package/dist/material-resolver.d.ts.map +1 -1
  91. package/dist/material-resolver.js +176 -386
  92. package/dist/material-resolver.js.map +1 -1
  93. package/dist/measure-unit-scale.d.ts +2 -0
  94. package/dist/measure-unit-scale.d.ts.map +1 -1
  95. package/dist/measure-unit-scale.js +7 -1
  96. package/dist/measure-unit-scale.js.map +1 -1
  97. package/dist/metadata-edit-value.d.ts +4 -0
  98. package/dist/metadata-edit-value.d.ts.map +1 -0
  99. package/dist/metadata-edit-value.js +57 -0
  100. package/dist/metadata-edit-value.js.map +1 -0
  101. package/dist/native-metadata-public.d.ts +9 -0
  102. package/dist/native-metadata-public.d.ts.map +1 -0
  103. package/dist/native-metadata-public.js +12 -0
  104. package/dist/native-metadata-public.js.map +1 -0
  105. package/dist/occurrence-quantity-read-error.d.ts +7 -0
  106. package/dist/occurrence-quantity-read-error.d.ts.map +1 -0
  107. package/dist/occurrence-quantity-read-error.js +13 -0
  108. package/dist/occurrence-quantity-read-error.js.map +1 -0
  109. package/dist/on-demand-extractors.d.ts +3 -38
  110. package/dist/on-demand-extractors.d.ts.map +1 -1
  111. package/dist/on-demand-extractors.js +4 -184
  112. package/dist/on-demand-extractors.js.map +1 -1
  113. package/dist/on-demand-georeferencing.d.ts.map +1 -1
  114. package/dist/on-demand-georeferencing.js +2 -0
  115. package/dist/on-demand-georeferencing.js.map +1 -1
  116. package/dist/parser-worker-trace.d.ts +18 -0
  117. package/dist/parser-worker-trace.d.ts.map +1 -0
  118. package/dist/parser-worker-trace.js +32 -0
  119. package/dist/parser-worker-trace.js.map +1 -0
  120. package/dist/parser.worker.d.ts.map +1 -1
  121. package/dist/parser.worker.js +34 -13
  122. package/dist/parser.worker.js.map +1 -1
  123. package/dist/project-units.d.ts +17 -4
  124. package/dist/project-units.d.ts.map +1 -1
  125. package/dist/project-units.js +104 -37
  126. package/dist/project-units.js.map +1 -1
  127. package/dist/property-complex-value.d.ts.map +1 -1
  128. package/dist/property-complex-value.js +1 -0
  129. package/dist/property-complex-value.js.map +1 -1
  130. package/dist/property-unit.d.ts +2 -0
  131. package/dist/property-unit.d.ts.map +1 -1
  132. package/dist/property-unit.js +4 -1
  133. package/dist/property-unit.js.map +1 -1
  134. package/dist/property-value-parser.d.ts +14 -1
  135. package/dist/property-value-parser.d.ts.map +1 -1
  136. package/dist/property-value-parser.js +42 -22
  137. package/dist/property-value-parser.js.map +1 -1
  138. package/dist/quantity-collect.d.ts +14 -2
  139. package/dist/quantity-collect.d.ts.map +1 -1
  140. package/dist/quantity-collect.js +19 -8
  141. package/dist/quantity-collect.js.map +1 -1
  142. package/dist/relationship-schema-slots.d.ts.map +1 -1
  143. package/dist/relationship-schema-slots.js +2 -1
  144. package/dist/relationship-schema-slots.js.map +1 -1
  145. package/dist/scan-worker-inline.d.ts.map +1 -1
  146. package/dist/scan-worker-inline.js +2 -1
  147. package/dist/scan-worker-inline.js.map +1 -1
  148. package/dist/schedule-extractor.d.ts +2 -1
  149. package/dist/schedule-extractor.d.ts.map +1 -1
  150. package/dist/schedule-extractor.js +11 -2
  151. package/dist/schedule-extractor.js.map +1 -1
  152. package/dist/schedule-serializer-builders.d.ts +7 -0
  153. package/dist/schedule-serializer-builders.d.ts.map +1 -1
  154. package/dist/schedule-serializer-builders.js +17 -17
  155. package/dist/schedule-serializer-builders.js.map +1 -1
  156. package/dist/schedule-serializer.d.ts +8 -3
  157. package/dist/schedule-serializer.d.ts.map +1 -1
  158. package/dist/schedule-serializer.js +36 -1
  159. package/dist/schedule-serializer.js.map +1 -1
  160. package/dist/schedule-task-products.d.ts +38 -0
  161. package/dist/schedule-task-products.d.ts.map +1 -0
  162. package/dist/schedule-task-products.js +69 -0
  163. package/dist/schedule-task-products.js.map +1 -0
  164. package/dist/schedule-types.d.ts +11 -1
  165. package/dist/schedule-types.d.ts.map +1 -1
  166. package/dist/schedule-types.js.map +1 -1
  167. package/dist/schema-entity-name.d.ts +14 -0
  168. package/dist/schema-entity-name.d.ts.map +1 -0
  169. package/dist/schema-entity-name.js +34 -0
  170. package/dist/schema-entity-name.js.map +1 -0
  171. package/dist/schema-exports.d.ts +7 -0
  172. package/dist/schema-exports.d.ts.map +1 -0
  173. package/dist/schema-exports.js +10 -0
  174. package/dist/schema-exports.js.map +1 -0
  175. package/dist/source-project-length-unit.d.ts +53 -0
  176. package/dist/source-project-length-unit.d.ts.map +1 -0
  177. package/dist/source-project-length-unit.js +201 -0
  178. package/dist/source-project-length-unit.js.map +1 -0
  179. package/dist/structural-extractor.js +2 -2
  180. package/dist/structural-extractor.js.map +1 -1
  181. package/dist/structural-load-extractor.d.ts +9 -3
  182. package/dist/structural-load-extractor.d.ts.map +1 -1
  183. package/dist/structural-load-extractor.js +26 -20
  184. package/dist/structural-load-extractor.js.map +1 -1
  185. package/dist/structural-step-values.d.ts +0 -9
  186. package/dist/structural-step-values.d.ts.map +1 -1
  187. package/dist/structural-step-values.js +13 -1
  188. package/dist/structural-step-values.js.map +1 -1
  189. package/dist/type-own-quantity-source.d.ts +37 -0
  190. package/dist/type-own-quantity-source.d.ts.map +1 -0
  191. package/dist/type-own-quantity-source.js +204 -0
  192. package/dist/type-own-quantity-source.js.map +1 -0
  193. package/dist/unit-extractor.d.ts +14 -1
  194. package/dist/unit-extractor.d.ts.map +1 -1
  195. package/dist/unit-extractor.js +34 -31
  196. package/dist/unit-extractor.js.map +1 -1
  197. package/dist/worker-parser.d.ts +7 -0
  198. package/dist/worker-parser.d.ts.map +1 -1
  199. package/dist/worker-parser.js +14 -16
  200. package/dist/worker-parser.js.map +1 -1
  201. package/package.json +6 -5
@@ -7,10 +7,12 @@
7
7
  * Includes cycle detection for recursive material references.
8
8
  */
9
9
  import { EntityExtractor } from './entity-extractor.js';
10
+ import { getReference, getReferences } from './attribute-helpers.js';
11
+ import { materialRecordReader, materialAssignmentState } from './material-overlay.js';
12
+ import { resolveMaterial } from './material-definitions.js';
10
13
  import { RelationshipType } from '@ifc-lite/data';
11
14
  import { isIfcTypeLikeEntity } from './columnar-parser-indexes.js';
12
- import { resolveEntityLengthUnitScale } from './unit-extractor.js';
13
- import { resolveAllMaterialDefIds, resolveMaterialOwnerAndDefIds, resolveOwnMaterialDefIds } from './material-associations.js';
15
+ import { resolveAllMaterialDefIds, resolveOwnMaterialDefIds } from './material-associations.js';
14
16
  export { resolveAllMaterialDefIds } from './material-associations.js';
15
17
  /**
16
18
  * Extract EVERY material association for an entity ON-DEMAND, resolved to
@@ -19,25 +21,29 @@ export { resolveAllMaterialDefIds } from './material-associations.js';
19
21
  * fallback IfcMaterial yield several. Order matches
20
22
  * {@link resolveAllMaterialDefIds}. Consumers that need a single value use
21
23
  * {@link extractMaterialsOnDemand} (=== element 0 here).
24
+ * An optional native view applies current assignments, aliases and material edits.
22
25
  */
23
- export function extractAllMaterialsOnDemand(store, entityId) {
24
- const { ownerId, defIds } = resolveMaterialOwnerAndDefIds(store, entityId);
25
- if (defIds.length === 0)
26
- return [];
27
- if (!store.source?.length) {
28
- const resolved = store.resolvedMaterials?.get(ownerId);
29
- // A missing or older wire row stays unverified. Never let a partial
30
- // forwarding payload convert an unknown value into a confident mismatch.
31
- return defIds.map((id) => resolved?.get(id) ?? { type: 'Material', unresolved: true });
32
- }
33
- const extractor = new EntityExtractor(store.source);
34
- const out = [];
35
- for (const defId of defIds) {
36
- const info = resolveMaterial(store, extractor, defId, new Set(), entityId);
37
- if (info)
38
- out.push(info);
39
- }
40
- return out;
26
+ export function extractAllMaterialsOnDemand(store, entityId, view) {
27
+ const { ownerId, defIds, complete } = materialAssignmentState(store, entityId, view);
28
+ const read = materialRecordReader(store, view);
29
+ return defIds.map((id) => {
30
+ const record = read(id);
31
+ if (!store.source?.length) {
32
+ const edits = view?.getEffectiveChanges?.().some(edit => {
33
+ if (!['attribute', 'type', 'entity-deleted'].includes(edit.kind))
34
+ return false;
35
+ // @raw-entity-enumeration-ok Immutable source kind identifies a current effective edit; material membership and values come from the effective reader.
36
+ const sourceType = store.entityIndex.byId.get(edit.entityId)?.type ?? store.deferredEntityIndex?.get(edit.entityId)?.type;
37
+ return edit.entityId === id || Boolean(sourceType?.toUpperCase().startsWith('IFCMATERIAL'));
38
+ });
39
+ const forwarded = complete && !edits ? store.resolvedMaterials?.get(ownerId)?.get(id) : undefined;
40
+ if (forwarded)
41
+ return forwarded;
42
+ if (!record || record.type.toUpperCase() !== 'IFCMATERIAL')
43
+ return { type: 'Material', unresolved: true };
44
+ }
45
+ return resolveMaterial(store, read, id, new Set(), view?.resolveBaseEntityId?.(entityId) ?? entityId) ?? { type: 'Material', unresolved: true };
46
+ });
41
47
  }
42
48
  /**
43
49
  * Extract materials for a single entity ON-DEMAND.
@@ -48,226 +54,9 @@ export function extractAllMaterialsOnDemand(store, entityId) {
48
54
  * Returns the entity's PRIMARY material (lowest-rel-express-id association);
49
55
  * use {@link extractAllMaterialsOnDemand} when every association matters.
50
56
  */
51
- export function extractMaterialsOnDemand(store, entityId) {
52
- const materialId = resolveAllMaterialDefIds(store, entityId)[0];
53
- if (materialId === undefined)
54
- return null;
55
- if (!store.source?.length) {
56
- const info = extractAllMaterialsOnDemand(store, entityId)[0];
57
- return info?.unresolved ? null : info ?? null;
58
- }
59
- const extractor = new EntityExtractor(store.source);
60
- return resolveMaterial(store, extractor, materialId, new Set(), entityId);
61
- }
62
- /**
63
- * Resolve a material entity by ID, handling all IFC material types. `visited`
64
- * guards cyclic *Usage references; `originEntityId` (the calling element/type)
65
- * lets a layer's thickness resolve its own project's unit scale below.
66
- */
67
- function resolveMaterial(store, extractor, materialId, visited = new Set(), originEntityId) {
68
- if (visited.has(materialId))
69
- return null;
70
- visited.add(materialId);
71
- // @raw-entity-enumeration-ok decode the selected material definition's one source STEP record
72
- const ref = store.entityIndex.byId.get(materialId);
73
- if (!ref)
74
- return null;
75
- const entity = extractor.extractEntity(ref);
76
- if (!entity)
77
- return null;
78
- const typeUpper = entity.type.toUpperCase();
79
- const attrs = entity.attributes || [];
80
- switch (typeUpper) {
81
- case 'IFCMATERIAL': {
82
- // IfcMaterial: [Name, Description, Category]
83
- return {
84
- type: 'Material',
85
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
86
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
87
- category: typeof attrs[2] === 'string' ? attrs[2] : undefined,
88
- };
89
- }
90
- case 'IFCMATERIALLAYERSET': {
91
- // IfcMaterialLayerSet: [MaterialLayers, LayerSetName, Description]
92
- const layerIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
93
- const layers = [];
94
- for (const layerId of layerIds) {
95
- // @raw-entity-enumeration-ok decode this material set's referenced layer record
96
- const layerRef = store.entityIndex.byId.get(layerId);
97
- if (!layerRef)
98
- continue;
99
- const layerEntity = extractor.extractEntity(layerRef);
100
- if (!layerEntity)
101
- continue;
102
- const la = layerEntity.attributes || [];
103
- // IfcMaterialLayer: [Material, LayerThickness, IsVentilated, Name, Description, Category, Priority]
104
- const matId = typeof la[0] === 'number' ? la[0] : undefined;
105
- let materialName;
106
- let materialCategory;
107
- if (matId) {
108
- // @raw-entity-enumeration-ok decode the layer's referenced material record
109
- const matRef = store.entityIndex.byId.get(matId);
110
- if (matRef) {
111
- const matEntity = extractor.extractEntity(matRef);
112
- if (matEntity) {
113
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
114
- materialCategory = typeof matEntity.attributes?.[2] === 'string' ? matEntity.attributes[2] : undefined;
115
- }
116
- }
117
- }
118
- // Convert raw IFC value to metres (a 60 mm slab must not read
119
- // "60.0 m"). `store.lengthUnitScale` answers for the file's
120
- // FIRST IfcProject only, wrong for a MergedExporter federated
121
- // layer in a LATER project — resolve per `originEntityId`.
122
- const rawThickness = typeof la[1] === 'number' ? la[1] : undefined;
123
- const scale = originEntityId !== undefined ? resolveEntityLengthUnitScale(store.source, store.entityIndex, store.relationships, originEntityId) : (store.lengthUnitScale ?? 1);
124
- const thickness = rawThickness !== undefined ? rawThickness * scale : undefined;
125
- layers.push({
126
- materialName,
127
- thickness,
128
- isVentilated: la[2] === true || la[2] === '.T.',
129
- name: typeof la[3] === 'string' ? la[3] : undefined,
130
- category: typeof la[5] === 'string' ? la[5] : undefined,
131
- materialCategory,
132
- });
133
- }
134
- return {
135
- type: 'MaterialLayerSet',
136
- name: typeof attrs[1] === 'string' ? attrs[1] : undefined,
137
- description: typeof attrs[2] === 'string' ? attrs[2] : undefined,
138
- layers,
139
- };
140
- }
141
- case 'IFCMATERIALPROFILESET': {
142
- // IfcMaterialProfileSet: [Name, Description, MaterialProfiles, CompositeProfile]
143
- const profileIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
144
- const profiles = [];
145
- for (const profId of profileIds) {
146
- // @raw-entity-enumeration-ok decode this profile set's referenced profile record
147
- const profRef = store.entityIndex.byId.get(profId);
148
- if (!profRef)
149
- continue;
150
- const profEntity = extractor.extractEntity(profRef);
151
- if (!profEntity)
152
- continue;
153
- const pa = profEntity.attributes || [];
154
- // IfcMaterialProfile: [Name, Description, Material, Profile, Priority, Category]
155
- const matId = typeof pa[2] === 'number' ? pa[2] : undefined;
156
- let materialName;
157
- let materialCategory;
158
- if (matId) {
159
- // @raw-entity-enumeration-ok decode the profile's referenced material record
160
- const matRef = store.entityIndex.byId.get(matId);
161
- if (matRef) {
162
- const matEntity = extractor.extractEntity(matRef);
163
- if (matEntity) {
164
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
165
- materialCategory = typeof matEntity.attributes?.[2] === 'string' ? matEntity.attributes[2] : undefined;
166
- }
167
- }
168
- }
169
- profiles.push({
170
- materialName,
171
- name: typeof pa[0] === 'string' ? pa[0] : undefined,
172
- category: typeof pa[5] === 'string' ? pa[5] : undefined,
173
- materialCategory,
174
- });
175
- }
176
- return {
177
- type: 'MaterialProfileSet',
178
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
179
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
180
- profiles,
181
- };
182
- }
183
- case 'IFCMATERIALCONSTITUENTSET': {
184
- // IfcMaterialConstituentSet: [Name, Description, MaterialConstituents]
185
- const constituentIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
186
- const constituents = [];
187
- for (const constId of constituentIds) {
188
- // @raw-entity-enumeration-ok decode this constituent set's referenced constituent record
189
- const constRef = store.entityIndex.byId.get(constId);
190
- if (!constRef)
191
- continue;
192
- const constEntity = extractor.extractEntity(constRef);
193
- if (!constEntity)
194
- continue;
195
- const ca = constEntity.attributes || [];
196
- // IfcMaterialConstituent: [Name, Description, Material, Fraction, Category]
197
- const matId = typeof ca[2] === 'number' ? ca[2] : undefined;
198
- let materialName;
199
- let materialCategory;
200
- if (matId) {
201
- // @raw-entity-enumeration-ok decode the constituent's referenced material record
202
- const matRef = store.entityIndex.byId.get(matId);
203
- if (matRef) {
204
- const matEntity = extractor.extractEntity(matRef);
205
- if (matEntity) {
206
- materialName = typeof matEntity.attributes?.[0] === 'string' ? matEntity.attributes[0] : undefined;
207
- // IfcMaterial: [Name, Description, Category] — IDS material
208
- // checks consider both the constituent's own category AND
209
- // the underlying IfcMaterial.Category as candidates for
210
- // a value match.
211
- materialCategory = typeof matEntity.attributes?.[2] === 'string' ? matEntity.attributes[2] : undefined;
212
- }
213
- }
214
- }
215
- constituents.push({
216
- materialName,
217
- name: typeof ca[0] === 'string' ? ca[0] : undefined,
218
- fraction: typeof ca[3] === 'number' ? ca[3] : undefined,
219
- category: typeof ca[4] === 'string' ? ca[4] : undefined,
220
- materialCategory,
221
- });
222
- }
223
- return {
224
- type: 'MaterialConstituentSet',
225
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
226
- description: typeof attrs[1] === 'string' ? attrs[1] : undefined,
227
- constituents,
228
- };
229
- }
230
- case 'IFCMATERIALLIST': {
231
- // IfcMaterialList: [Materials]
232
- const matIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
233
- const materials = [];
234
- for (const matId of matIds) {
235
- // @raw-entity-enumeration-ok decode this material list's referenced material record
236
- const matRef = store.entityIndex.byId.get(matId);
237
- if (!matRef)
238
- continue;
239
- const matEntity = extractor.extractEntity(matRef);
240
- if (matEntity) {
241
- const name = typeof matEntity.attributes?.[0] === 'string'
242
- ? matEntity.attributes[0] : `Material #${matId}`;
243
- const category = typeof matEntity.attributes?.[2] === 'string' ? matEntity.attributes[2] : undefined;
244
- materials.push({ name, ...(category ? { category } : {}) });
245
- }
246
- }
247
- return {
248
- type: 'MaterialList',
249
- materials,
250
- };
251
- }
252
- case 'IFCMATERIALLAYERSETUSAGE': {
253
- // IfcMaterialLayerSetUsage: [ForLayerSet, LayerSetDirection, DirectionSense, OffsetFromReferenceLine, ...]
254
- const layerSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
255
- if (layerSetId) {
256
- return resolveMaterial(store, extractor, layerSetId, visited, originEntityId);
257
- }
258
- return null;
259
- }
260
- case 'IFCMATERIALPROFILESETUSAGE': {
261
- // IfcMaterialProfileSetUsage: [ForProfileSet, ...]
262
- const profileSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
263
- if (profileSetId) {
264
- return resolveMaterial(store, extractor, profileSetId, visited, originEntityId);
265
- }
266
- return null;
267
- }
268
- default:
269
- return null;
270
- }
57
+ export function extractMaterialsOnDemand(store, entityId, view) {
58
+ const first = extractAllMaterialsOnDemand(store, entityId, view)[0];
59
+ return first?.unresolved ? null : first ?? null;
271
60
  }
272
61
  /** Resolve an entity ref from the primary index, falling back to deferred atoms. */
273
62
  function getRef(store, id) {
@@ -275,11 +64,8 @@ function getRef(store, id) {
275
64
  return store.entityIndex.byId.get(id) ?? store.deferredEntityIndex?.get(id);
276
65
  }
277
66
  /** Read an IfcMaterial's Name (attr 0) and Category (attr 2). */
278
- function readMaterialNameCategory(store, extractor, materialId) {
279
- const ref = getRef(store, materialId);
280
- if (!ref)
281
- return {};
282
- const entity = extractor.extractEntity(ref);
67
+ function readMaterialNameCategory(read, materialId) {
68
+ const entity = read(materialId);
283
69
  const attrs = entity?.attributes ?? [];
284
70
  return {
285
71
  name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
@@ -303,7 +89,9 @@ const leavesCache = new WeakMap();
303
89
  * leaf with weight 1. Results are memoised per (store, defId) — a type-shared
304
90
  * layer set is resolved once for the whole model.
305
91
  */
306
- export function collectMaterialLeaves(store, defId) {
92
+ export function collectMaterialLeaves(store, defId, view) {
93
+ if (view)
94
+ return resolveLeaves(materialRecordReader(store, view), defId, new Set());
307
95
  let cache = leavesCache.get(store);
308
96
  if (!cache) {
309
97
  cache = new Map();
@@ -312,10 +100,9 @@ export function collectMaterialLeaves(store, defId) {
312
100
  const cached = cache.get(defId);
313
101
  if (cached)
314
102
  return cached;
315
- const extractor = store.source?.length ? new EntityExtractor(store.source) : null;
316
103
  let result;
317
- if (extractor) {
318
- result = resolveLeaves(store, extractor, defId, new Set());
104
+ if (store.source?.length) {
105
+ result = resolveLeaves(materialRecordReader(store), defId, new Set());
319
106
  }
320
107
  else {
321
108
  // Source-less store (server-loaded models keep `source` as an EMPTY
@@ -351,147 +138,145 @@ function mergeLeaves(into, leaf) {
351
138
  }
352
139
  /** Recursively resolve a material definition into weighted base-material leaves
353
140
  * (cycle-guarded via `visited`). See {@link collectMaterialLeaves}. */
354
- function resolveLeaves(store, extractor, defId, visited) {
355
- if (visited.has(defId))
356
- return [];
357
- visited.add(defId);
358
- const ref = getRef(store, defId);
359
- if (!ref)
360
- return [];
361
- const entity = extractor.extractEntity(ref);
362
- if (!entity)
363
- return [];
364
- const typeUpper = entity.type.toUpperCase();
365
- const attrs = entity.attributes || [];
366
- const merged = new Map();
367
- const addMaterialLeaf = (matId, weight) => {
368
- if (matId === undefined)
369
- return;
370
- const { name, category } = readMaterialNameCategory(store, extractor, matId);
371
- mergeLeaves(merged, { id: matId, name, category, weight });
372
- };
373
- switch (typeUpper) {
374
- case 'IFCMATERIAL':
375
- return [{
376
- id: defId,
377
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
378
- category: typeof attrs[2] === 'string' ? attrs[2] : undefined,
379
- weight: 1,
380
- }];
381
- case 'IFCMATERIALLAYERSET': {
382
- // IfcMaterialLayerSet: [MaterialLayers, LayerSetName, Description]
383
- const layerIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
384
- const layers = [];
385
- for (const layerId of layerIds) {
386
- const layerRef = getRef(store, layerId);
387
- if (!layerRef)
388
- continue;
389
- const la = extractor.extractEntity(layerRef)?.attributes ?? [];
390
- // IfcMaterialLayer: [Material, LayerThickness, IsVentilated, Name, Description, Category, Priority]
391
- layers.push({
392
- matId: typeof la[0] === 'number' ? la[0] : undefined,
393
- // Non-finite thickness would turn every weight into NaN
394
- // (Infinity/Infinity) - treat it as absent like <= 0.
395
- thickness: typeof la[1] === 'number' && Number.isFinite(la[1]) && la[1] > 0 ? la[1] : 0,
396
- });
397
- }
398
- const totalThickness = layers.reduce((s, l) => s + l.thickness, 0);
399
- for (const layer of layers) {
400
- const weight = totalThickness > 0
401
- ? layer.thickness / totalThickness
402
- : (layers.length > 0 ? 1 / layers.length : 0);
403
- addMaterialLeaf(layer.matId, weight);
404
- }
405
- return [...merged.values()];
141
+ function resolveLeaves(read, defId, visited) {
142
+ while (true) {
143
+ if (visited.has(defId))
144
+ return [];
145
+ visited.add(defId);
146
+ const entity = read(defId);
147
+ if (!entity)
148
+ return [];
149
+ const typeUpper = entity.type.toUpperCase();
150
+ const attrs = entity.attributes || [];
151
+ if (typeUpper === 'IFCMATERIALLAYERSETUSAGE' || typeUpper === 'IFCMATERIALPROFILESETUSAGE') {
152
+ const next = getReference(attrs[0]);
153
+ if (next === undefined)
154
+ return [];
155
+ defId = next;
156
+ continue;
406
157
  }
407
- case 'IFCMATERIALPROFILESET': {
408
- // IfcMaterialProfileSet: [Name, Description, MaterialProfiles, CompositeProfile]
409
- const profileIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
410
- const n = profileIds.length;
411
- for (const profId of profileIds) {
412
- const profRef = getRef(store, profId);
413
- if (!profRef)
414
- continue;
415
- const pa = extractor.extractEntity(profRef)?.attributes ?? [];
416
- // IfcMaterialProfile: [Name, Description, Material, Profile, Priority, Category]
417
- addMaterialLeaf(typeof pa[2] === 'number' ? pa[2] : undefined, n > 0 ? 1 / n : 0);
158
+ const merged = new Map();
159
+ const addMaterialLeaf = (matId, weight) => {
160
+ if (matId === undefined)
161
+ return;
162
+ const { name, category } = readMaterialNameCategory(read, matId);
163
+ mergeLeaves(merged, { id: matId, name, category, weight });
164
+ };
165
+ switch (typeUpper) {
166
+ case 'IFCMATERIAL':
167
+ return [{
168
+ id: defId,
169
+ name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
170
+ category: typeof attrs[2] === 'string' ? attrs[2] : undefined,
171
+ weight: 1,
172
+ }];
173
+ case 'IFCMATERIALLAYERSET': {
174
+ // IfcMaterialLayerSet: [MaterialLayers, LayerSetName, Description]
175
+ const layerIds = getReferences(attrs[0]) ?? [];
176
+ const layers = [];
177
+ for (const layerId of layerIds) {
178
+ const layerRef = read(layerId);
179
+ if (!layerRef)
180
+ continue;
181
+ const la = layerRef.attributes ?? [];
182
+ // IfcMaterialLayer: [Material, LayerThickness, IsVentilated, Name, Description, Category, Priority]
183
+ layers.push({
184
+ matId: getReference(la[0]),
185
+ // Non-finite thickness would turn every weight into NaN
186
+ // (Infinity/Infinity) - treat it as absent like <= 0.
187
+ thickness: typeof la[1] === 'number' && Number.isFinite(la[1]) && la[1] > 0 ? la[1] : 0,
188
+ });
189
+ }
190
+ const totalThickness = layers.reduce((s, l) => s + l.thickness, 0);
191
+ for (const layer of layers) {
192
+ const weight = totalThickness > 0
193
+ ? layer.thickness / totalThickness
194
+ : (layers.length > 0 ? 1 / layers.length : 0);
195
+ addMaterialLeaf(layer.matId, weight);
196
+ }
197
+ return [...merged.values()];
418
198
  }
419
- return [...merged.values()];
420
- }
421
- case 'IFCMATERIALCONSTITUENTSET': {
422
- // IfcMaterialConstituentSet: [Name, Description, MaterialConstituents]
423
- const constIds = Array.isArray(attrs[2]) ? attrs[2].filter((id) => typeof id === 'number') : [];
424
- const constituents = [];
425
- for (const constId of constIds) {
426
- const constRef = getRef(store, constId);
427
- if (!constRef)
428
- continue;
429
- const ca = extractor.extractEntity(constRef)?.attributes ?? [];
430
- // IfcMaterialConstituent: [Name, Description, Material, Fraction, Category]
431
- // An authored Fraction of 0 is preserved as 0 (an explicit
432
- // "contributes nothing"), distinct from an ABSENT fraction.
433
- constituents.push({
434
- matId: typeof ca[2] === 'number' ? ca[2] : undefined,
435
- // Non-finite (NaN/Infinity) and negative fractions are
436
- // malformed for an IfcNormalisedRatioMeasure — treat them
437
- // as unset so they can't poison the weight arithmetic
438
- // below. An authored 0.0 is legal and PRESERVED: it is an
439
- // explicit "contributes nothing", distinct from absent.
440
- fraction: typeof ca[3] === 'number' && Number.isFinite(ca[3]) && ca[3] >= 0 ? ca[3] : undefined,
441
- });
199
+ case 'IFCMATERIALPROFILESET': {
200
+ // IfcMaterialProfileSet: [Name, Description, MaterialProfiles, CompositeProfile]
201
+ const profileIds = getReferences(attrs[2]) ?? [];
202
+ const n = profileIds.length;
203
+ for (const profId of profileIds) {
204
+ const profRef = read(profId);
205
+ if (!profRef)
206
+ continue;
207
+ const pa = profRef.attributes ?? [];
208
+ // IfcMaterialProfile: [Name, Description, Material, Profile, Priority, Category]
209
+ addMaterialLeaf(getReference(pa[2]), n > 0 ? 1 / n : 0);
210
+ }
211
+ return [...merged.values()];
442
212
  }
443
- // Constituent Fraction is optional per-constituent. Constituents
444
- // WITHOUT an explicit fraction share whatever remains of the whole
445
- // (1 - sum of explicit) evenly, so they must not collapse to
446
- // weight 0 and vanish from the totals. When the explicit fractions
447
- // already fill (or overflow) the whole, each implicit sibling gets
448
- // an even 1/n share instead so it still registers. The set is then
449
- // renormalised to sum to exactly 1 (e.g. {1.0, unset} would
450
- // otherwise weigh 1.5x the element), so the totals panel can never
451
- // over-report an element's quantities. Explicit zeros survive all
452
- // branches: an all-explicit-zero set keeps every weight at 0
453
- // rather than inventing equal shares.
454
- const explicitTotal = constituents.reduce((s, c) => s + (c.fraction ?? 0), 0);
455
- const implicitCount = constituents.reduce((n, c) => n + (c.fraction === undefined ? 1 : 0), 0);
456
- const remaining = 1 - explicitTotal;
457
- const perImplicit = implicitCount > 0
458
- ? (remaining > 0 ? remaining / implicitCount : 1 / constituents.length)
459
- : 0;
460
- const provisional = constituents.map((c) => c.fraction ?? perImplicit);
461
- const totalWeight = provisional.reduce((s, w) => s + w, 0);
462
- for (let i = 0; i < constituents.length; i++) {
463
- // totalWeight can only be 0 when EVERY fraction is an authored
464
- // 0.0 (absent fractions receive a positive share) — keep those
465
- // explicit zeros instead of inventing equal weights.
466
- const weight = totalWeight > 0 ? provisional[i] / totalWeight : 0;
467
- addMaterialLeaf(constituents[i].matId, weight);
213
+ case 'IFCMATERIALCONSTITUENTSET': {
214
+ // IfcMaterialConstituentSet: [Name, Description, MaterialConstituents]
215
+ const constIds = getReferences(attrs[2]) ?? [];
216
+ const constituents = [];
217
+ for (const constId of constIds) {
218
+ const constRef = read(constId);
219
+ if (!constRef)
220
+ continue;
221
+ const ca = constRef.attributes ?? [];
222
+ // IfcMaterialConstituent: [Name, Description, Material, Fraction, Category]
223
+ // An authored Fraction of 0 is preserved as 0 (an explicit
224
+ // "contributes nothing"), distinct from an ABSENT fraction.
225
+ constituents.push({
226
+ matId: getReference(ca[2]),
227
+ // Non-finite (NaN/Infinity) and negative fractions are
228
+ // malformed for an IfcNormalisedRatioMeasure — treat them
229
+ // as unset so they can't poison the weight arithmetic
230
+ // below. An authored 0.0 is legal and PRESERVED: it is an
231
+ // explicit "contributes nothing", distinct from absent.
232
+ fraction: typeof ca[3] === 'number' && Number.isFinite(ca[3]) && ca[3] >= 0 ? ca[3] : undefined,
233
+ });
234
+ }
235
+ // Constituent Fraction is optional per-constituent. Constituents
236
+ // WITHOUT an explicit fraction share whatever remains of the whole
237
+ // (1 - sum of explicit) evenly, so they must not collapse to
238
+ // weight 0 and vanish from the totals. When the explicit fractions
239
+ // already fill (or overflow) the whole, each implicit sibling gets
240
+ // an even 1/n share instead so it still registers. The set is then
241
+ // renormalised to sum to exactly 1 (e.g. {1.0, unset} would
242
+ // otherwise weigh 1.5x the element), so the totals panel can never
243
+ // over-report an element's quantities. Explicit zeros survive all
244
+ // branches: an all-explicit-zero set keeps every weight at 0
245
+ // rather than inventing equal shares.
246
+ const explicitTotal = constituents.reduce((s, c) => s + (c.fraction ?? 0), 0);
247
+ const implicitCount = constituents.reduce((n, c) => n + (c.fraction === undefined ? 1 : 0), 0);
248
+ const remaining = 1 - explicitTotal;
249
+ const perImplicit = implicitCount > 0
250
+ ? (remaining > 0 ? remaining / implicitCount : 1 / constituents.length)
251
+ : 0;
252
+ const provisional = constituents.map((c) => c.fraction ?? perImplicit);
253
+ const totalWeight = provisional.reduce((s, w) => s + w, 0);
254
+ for (let i = 0; i < constituents.length; i++) {
255
+ // totalWeight can only be 0 when EVERY fraction is an authored
256
+ // 0.0 (absent fractions receive a positive share) — keep those
257
+ // explicit zeros instead of inventing equal weights.
258
+ const weight = totalWeight > 0 ? provisional[i] / totalWeight : 0;
259
+ addMaterialLeaf(constituents[i].matId, weight);
260
+ }
261
+ return [...merged.values()];
468
262
  }
469
- return [...merged.values()];
470
- }
471
- case 'IFCMATERIALLIST': {
472
- // IfcMaterialList: [Materials]
473
- const matIds = Array.isArray(attrs[0]) ? attrs[0].filter((id) => typeof id === 'number') : [];
474
- const n = matIds.length;
475
- for (const matId of matIds)
476
- addMaterialLeaf(matId, n > 0 ? 1 / n : 0);
477
- return [...merged.values()];
478
- }
479
- case 'IFCMATERIALLAYERSETUSAGE': {
480
- const layerSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
481
- return layerSetId ? resolveLeaves(store, extractor, layerSetId, visited) : [];
482
- }
483
- case 'IFCMATERIALPROFILESETUSAGE': {
484
- const profileSetId = typeof attrs[0] === 'number' ? attrs[0] : undefined;
485
- return profileSetId ? resolveLeaves(store, extractor, profileSetId, visited) : [];
263
+ case 'IFCMATERIALLIST': {
264
+ // IfcMaterialList: [Materials]
265
+ const matIds = getReferences(attrs[0]) ?? [];
266
+ const n = matIds.length;
267
+ for (const matId of matIds)
268
+ addMaterialLeaf(matId, n > 0 ? 1 / n : 0);
269
+ return [...merged.values()];
270
+ }
271
+ default:
272
+ // Unknown definition — treat as a single opaque material so it still
273
+ // appears in the tab and carries the element's full volume.
274
+ return [{
275
+ id: defId,
276
+ name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
277
+ weight: 1,
278
+ }];
486
279
  }
487
- default:
488
- // Unknown definition — treat as a single opaque material so it still
489
- // appears in the tab and carries the element's full volume.
490
- return [{
491
- id: defId,
492
- name: typeof attrs[0] === 'string' ? attrs[0] : undefined,
493
- weight: 1,
494
- }];
495
280
  }
496
281
  }
497
282
  const usageIndexCache = new WeakMap();
@@ -648,7 +433,12 @@ export function buildMaterialUsageIndex(store) {
648
433
  const displayCache = new WeakMap();
649
434
  /** Resolve a material entity's display name + IFC class for headers/labels.
650
435
  * Memoised per (store, materialId) — callers resolve many materials in a loop. */
651
- export function getMaterialDisplay(store, materialId) {
436
+ export function getMaterialDisplay(store, materialId, view) {
437
+ if (view) {
438
+ const record = materialRecordReader(store, view)(materialId);
439
+ return { name: typeof record?.attributes[0] === 'string' ? record.attributes[0] : `Material #${materialId}`,
440
+ type: record?.type ?? 'IfcMaterial' };
441
+ }
652
442
  let cache = displayCache.get(store);
653
443
  if (!cache) {
654
444
  cache = new Map();