@ifc-lite/parser 6.5.0 → 7.1.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 (106) hide show
  1. package/README.md +21 -0
  2. package/dist/columnar-parser-root-attributes.d.ts +2 -5
  3. package/dist/columnar-parser-root-attributes.d.ts.map +1 -1
  4. package/dist/columnar-parser-root-attributes.js +22 -2
  5. package/dist/columnar-parser-root-attributes.js.map +1 -1
  6. package/dist/columnar-parser.d.ts +4 -0
  7. package/dist/columnar-parser.d.ts.map +1 -1
  8. package/dist/columnar-parser.js +2 -47
  9. package/dist/columnar-parser.js.map +1 -1
  10. package/dist/cost-category-buckets.d.ts +5 -0
  11. package/dist/cost-category-buckets.d.ts.map +1 -0
  12. package/dist/cost-category-buckets.js +37 -0
  13. package/dist/cost-category-buckets.js.map +1 -0
  14. package/dist/cost-compatibility-tree.d.ts +4 -0
  15. package/dist/cost-compatibility-tree.d.ts.map +1 -0
  16. package/dist/cost-compatibility-tree.js +46 -0
  17. package/dist/cost-compatibility-tree.js.map +1 -0
  18. package/dist/cost-evaluation-arithmetic.d.ts +16 -0
  19. package/dist/cost-evaluation-arithmetic.d.ts.map +1 -0
  20. package/dist/cost-evaluation-arithmetic.js +181 -0
  21. package/dist/cost-evaluation-arithmetic.js.map +1 -0
  22. package/dist/cost-evaluation-context.d.ts +20 -0
  23. package/dist/cost-evaluation-context.d.ts.map +1 -0
  24. package/dist/cost-evaluation-context.js +54 -0
  25. package/dist/cost-evaluation-context.js.map +1 -0
  26. package/dist/cost-evaluation-result.d.ts +5 -0
  27. package/dist/cost-evaluation-result.d.ts.map +1 -0
  28. package/dist/cost-evaluation-result.js +20 -0
  29. package/dist/cost-evaluation-result.js.map +1 -0
  30. package/dist/cost-evaluator.d.ts +6 -0
  31. package/dist/cost-evaluator.d.ts.map +1 -0
  32. package/dist/cost-evaluator.js +398 -0
  33. package/dist/cost-evaluator.js.map +1 -0
  34. package/dist/cost-extractor.d.ts +4 -8
  35. package/dist/cost-extractor.d.ts.map +1 -1
  36. package/dist/cost-extractor.js +346 -334
  37. package/dist/cost-extractor.js.map +1 -1
  38. package/dist/cost-overlay.d.ts +72 -0
  39. package/dist/cost-overlay.d.ts.map +1 -0
  40. package/dist/cost-overlay.js +5 -0
  41. package/dist/cost-overlay.js.map +1 -0
  42. package/dist/cost-quantities.d.ts +5 -0
  43. package/dist/cost-quantities.d.ts.map +1 -0
  44. package/dist/cost-quantities.js +116 -0
  45. package/dist/cost-quantities.js.map +1 -0
  46. package/dist/cost-quantity-evaluator.d.ts +23 -0
  47. package/dist/cost-quantity-evaluator.d.ts.map +1 -0
  48. package/dist/cost-quantity-evaluator.js +153 -0
  49. package/dist/cost-quantity-evaluator.js.map +1 -0
  50. package/dist/cost-reader.d.ts +59 -0
  51. package/dist/cost-reader.d.ts.map +1 -0
  52. package/dist/cost-reader.js +210 -0
  53. package/dist/cost-reader.js.map +1 -0
  54. package/dist/cost-relationships.d.ts +7 -0
  55. package/dist/cost-relationships.d.ts.map +1 -0
  56. package/dist/cost-relationships.js +237 -0
  57. package/dist/cost-relationships.js.map +1 -0
  58. package/dist/cost-step-lexemes.d.ts +16 -0
  59. package/dist/cost-step-lexemes.d.ts.map +1 -0
  60. package/dist/cost-step-lexemes.js +131 -0
  61. package/dist/cost-step-lexemes.js.map +1 -0
  62. package/dist/cost-types.d.ts +218 -76
  63. package/dist/cost-types.d.ts.map +1 -1
  64. package/dist/cost-units.d.ts +38 -0
  65. package/dist/cost-units.d.ts.map +1 -0
  66. package/dist/cost-units.js +348 -0
  67. package/dist/cost-units.js.map +1 -0
  68. package/dist/cost-value-evaluation-session.d.ts +19 -0
  69. package/dist/cost-value-evaluation-session.d.ts.map +1 -0
  70. package/dist/cost-value-evaluation-session.js +25 -0
  71. package/dist/cost-value-evaluation-session.js.map +1 -0
  72. package/dist/cost.d.ts +5 -0
  73. package/dist/cost.d.ts.map +1 -0
  74. package/dist/cost.js +6 -0
  75. package/dist/cost.js.map +1 -0
  76. package/dist/entity-extractor.d.ts.map +1 -1
  77. package/dist/entity-extractor.js +5 -2
  78. package/dist/entity-extractor.js.map +1 -1
  79. package/dist/index.d.ts +1 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/on-demand-extractors.d.ts +3 -4
  84. package/dist/on-demand-extractors.d.ts.map +1 -1
  85. package/dist/on-demand-extractors.js +9 -6
  86. package/dist/on-demand-extractors.js.map +1 -1
  87. package/dist/project-units.d.ts.map +1 -1
  88. package/dist/project-units.js +8 -2
  89. package/dist/project-units.js.map +1 -1
  90. package/dist/property-unit.d.ts +24 -0
  91. package/dist/property-unit.d.ts.map +1 -0
  92. package/dist/property-unit.js +29 -0
  93. package/dist/property-unit.js.map +1 -0
  94. package/dist/step-entity-parameters.d.ts +9 -0
  95. package/dist/step-entity-parameters.d.ts.map +1 -0
  96. package/dist/step-entity-parameters.js +30 -0
  97. package/dist/step-entity-parameters.js.map +1 -0
  98. package/dist/worker-parser.d.ts +29 -1
  99. package/dist/worker-parser.d.ts.map +1 -1
  100. package/dist/worker-parser.js +80 -7
  101. package/dist/worker-parser.js.map +1 -1
  102. package/package.json +6 -5
  103. package/dist/cost-schedule-2x3.d.ts +0 -31
  104. package/dist/cost-schedule-2x3.d.ts.map +0 -1
  105. package/dist/cost-schedule-2x3.js +0 -75
  106. package/dist/cost-schedule-2x3.js.map +0 -1
@@ -1,369 +1,381 @@
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
- * Cost (5D) extractor — parses IfcCostItem, IfcCostValue, IfcCostSchedule,
6
- * IfcRelNests and IfcRelAssignsToControl from a parsed IfcDataStore into a
7
- * normalized CostExtraction. Structure mirrors `schedule-extractor.ts`
8
- * (schedule/task hierarchy -> cost/cost-item hierarchy).
9
- *
10
- * Relationship wiring, verified against `packages/codegen/schemas/IFC4_ADD2_TC1.exp`
11
- * / `IFC4X3.exp` (identical in both):
12
- *
13
- * - `IfcControl` (the supertype of both `IfcCostItem` and `IfcCostSchedule`)
14
- * declares `INVERSE Controls : SET [0:?] OF IfcRelAssignsToControl FOR
15
- * RelatingControl` — `IfcRelAssignsToControl` is the ONLY relationship
16
- * that can target a Control as `RelatingControl`, so it is necessarily
17
- * what binds both "schedule controls cost items" and "cost item controls
18
- * the objects it prices" — there is no separate relationship analogous to
19
- * `IfcRelAssignsToProcess` for cost. Which of the two a given
20
- * `IfcRelAssignsToControl` instance expresses is told apart here by
21
- * what its `RelatingControl` expressId resolves to (a cost schedule or
22
- * a cost item), not by anything the schema itself distinguishes.
23
- * - Cost item breakdown (parent/child `IfcCostItem` nesting) uses
24
- * `IfcRelNests` (`RelatingObject`/`RelatedObjects : IfcObjectDefinition`,
25
- * which `IfcCostItem` satisfies via `IfcControl -> IfcObject ->
26
- * IfcObjectDefinition`) — the same relationship `schedule-extractor.ts`
27
- * uses for task hierarchy. The EXPRESS schema does not constrain this by
28
- * a WHERE rule (nothing stops `IfcRelNests` from nesting unrelated
29
- * objects), so this is the domain convention this extractor follows, not
30
- * a schema-enforced fact — reported explicitly rather than asserted as
31
- * certain.
32
- *
33
- * IFC2X3's `IfcCostItem` has NO attributes at all (`SUBTYPE OF (IfcControl)`
34
- * only — no PredefinedType/CostValues/CostQuantities), so those fields are
35
- * always `undefined` for a 2X3 file; this is schema-version-gated below, not
36
- * inferred from an empty read. IFC2X3's `IfcCostSchedule` DOES carry
37
- * Status/SubmittedOn/UpdateDate/PredefinedType, at different slots and with
38
- * entity-valued dates — read through `cost-schedule-2x3.ts`.
39
- */
40
- import { EntityExtractor } from './entity-extractor.js';
41
- import { collectQuantitiesFromRefs } from './quantity-collect.js';
42
- import { resolveUnitByRef } from './project-units.js';
43
- import { COST_SCHEDULE_ATTR_2X3, resolveDateTimeSelect2x3 } from './cost-schedule-2x3.js';
44
- /** Flattened IFC4/IFC4X3 STEP attribute indices for IfcCostItem. */
45
- const COST_ITEM_ATTR = {
46
- GlobalId: 0,
47
- Name: 2,
48
- Description: 3,
49
- ObjectType: 4,
50
- Identification: 5,
51
- PredefinedType: 6,
52
- CostValues: 7,
53
- CostQuantities: 8,
54
- };
55
- const COST_SCHEDULE_ATTR = {
56
- GlobalId: 0,
57
- Name: 2,
58
- Description: 3,
59
- ObjectType: 4,
60
- Identification: 5,
61
- PredefinedType: 6,
62
- Status: 7,
63
- SubmittedOn: 8,
64
- UpdateDate: 9,
65
- };
66
- /**
67
- * IfcAppliedValue attribute slots (IfcCostValue adds no attributes of its
68
- * own). Slot 3, `UnitBasis : OPTIONAL IfcMeasureWithUnit`, is what tells a
69
- * RATE ("$85 per hour") apart from a flat total ("$5,000") — read via
70
- * `extractUnitBasis` below, not skipped.
71
- */
72
- const COST_VALUE_ATTR = {
73
- Name: 0,
74
- Description: 1,
75
- AppliedValue: 2,
76
- UnitBasis: 3,
77
- ApplicableDate: 4,
78
- FixedUntilDate: 5,
79
- Category: 6,
80
- Condition: 7,
81
- ArithmeticOperator: 8,
82
- Components: 9,
83
- };
84
- const REL_NESTS_ATTR = { RelatingObject: 4, RelatedObjects: 5 };
85
- const REL_ASSIGNS_TO_CONTROL_ATTR = { RelatedObjects: 4, RelatingControl: 6 };
86
- function asString(v) {
87
- if (typeof v === 'string' && v.length > 0)
88
- return v;
89
- return undefined;
4
+ import { QuantityType } from '@ifc-lite/data';
5
+ import { compatibilityTreeBuilder } from './cost-compatibility-tree.js';
6
+ import { costQuantityExactValue, extractCostQuantities } from './cost-quantities.js';
7
+ import { asEnum, asRef, asString, CostEntityReader } from './cost-reader.js';
8
+ import { diagnoseCostGraphs, extractCostRelationships } from './cost-relationships.js';
9
+ import { isZeroCostNumericLexeme } from './cost-step-lexemes.js';
10
+ import { getInheritanceChain } from './ifc-schema.js';
11
+ import { CostUnitResolver } from './cost-units.js';
12
+ function scalarString(value) {
13
+ if (Array.isArray(value) && value.length === 2)
14
+ return scalarString(value[1]);
15
+ if (typeof value === 'string' && value.length > 0)
16
+ return value;
17
+ return typeof value === 'number' && Number.isFinite(value) ? value.toString() : undefined;
90
18
  }
91
- function asEnum(v) {
92
- if (typeof v !== 'string' || v.length === 0)
93
- return undefined;
94
- const match = v.match(/^\.([A-Z_]+)\.$/);
95
- return match ? match[1] : undefined;
96
- }
97
- function asRef(v) {
98
- if (typeof v === 'number' && Number.isInteger(v) && v > 0)
99
- return v;
100
- return undefined;
101
- }
102
- function asRefList(v) {
103
- if (!Array.isArray(v))
104
- return [];
105
- const out = [];
106
- for (const x of v) {
107
- const id = asRef(x);
108
- if (id !== undefined)
109
- out.push(id);
110
- }
111
- return out;
112
- }
113
- /**
114
- * `AppliedValue` is an `IfcAppliedValueSelect`. A resolved measure reads as a
115
- * typed pair `['IFCMONETARYMEASURE', 500]` (same shape `extractLagTimeSeconds`
116
- * in `schedule-extractor.ts` unwraps for `IfcTimeOrRatioSelect`); a plain
117
- * number is accepted too, since not every parse path wraps a select. Any
118
- * other select member (e.g. a table or measure-with-unit reference) is left
119
- * unresolved rather than guessed at.
120
- */
121
- function asAppliedValueNumber(v) {
122
- if (typeof v === 'number' && Number.isFinite(v))
123
- return v;
124
- if (Array.isArray(v) && v.length === 2) {
125
- const inner = v[1];
126
- if (typeof inner === 'number' && Number.isFinite(inner))
127
- return inner;
128
- if (typeof inner === 'string') {
129
- const n = parseFloat(inner);
130
- if (Number.isFinite(n))
131
- return n;
19
+ function dateTime2x3(reader, value) {
20
+ const pad = (part) => String(part).padStart(2, '0');
21
+ let current = asRef(value);
22
+ let clock;
23
+ const visited = new Set();
24
+ while (current !== undefined) {
25
+ if (visited.has(current))
26
+ return undefined;
27
+ visited.add(current);
28
+ const entity = reader.get(current);
29
+ if (!entity)
30
+ return undefined;
31
+ const a = entity.attributes ?? [];
32
+ const type = entity.type.toUpperCase();
33
+ if (type === 'IFCCALENDARDATE' &&
34
+ typeof a[0] === 'number' && typeof a[1] === 'number' && typeof a[2] === 'number') {
35
+ const date = `${a[2]}-${pad(a[1])}-${pad(a[0])}`;
36
+ return clock ? `${date}T${clock}` : date;
37
+ }
38
+ if (type !== 'IFCDATEANDTIME')
39
+ return undefined;
40
+ if (!clock) {
41
+ const timeId = asRef(a[1]);
42
+ const time = timeId === undefined ? undefined : reader.get(timeId);
43
+ const t = time?.type.toUpperCase() === 'IFCLOCALTIME' ? time.attributes ?? [] : [];
44
+ if (typeof t[0] === 'number') {
45
+ const minute = typeof t[1] === 'number' ? t[1] : 0;
46
+ const second = typeof t[2] === 'number' ? Math.floor(t[2]) : 0;
47
+ clock = `${pad(t[0])}:${pad(minute)}:${pad(second)}`;
48
+ }
132
49
  }
50
+ current = asRef(a[0]);
133
51
  }
134
52
  return undefined;
135
53
  }
136
- /**
137
- * Resolve `IfcAppliedValue.UnitBasis` (slot 3, `OPTIONAL IfcMeasureWithUnit`)
138
- * into the minimal shape that answers "is this a rate or a total?" —
139
- * `undefined` when `UnitBasis` is absent or its reference is broken;
140
- * otherwise an object whose mere presence marks the owning `CostValueInfo` as
141
- * a rate, with `valueComponent`/`unitSymbol` individually `undefined` when
142
- * that half of `IfcMeasureWithUnit` could not itself be resolved (see
143
- * {@link CostValueUnitBasis}'s doc comment in `cost-types.ts`).
144
- *
145
- * `IfcMeasureWithUnit` (`ValueComponent : IfcValue`, `UnitComponent :
146
- * IfcUnit`) — both SELECT types, so each is resolved independently:
147
- * - `ValueComponent` reuses `asAppliedValueNumber`, the same resolution
148
- * `AppliedValue` above uses for its own `IfcAppliedValueSelect`.
149
- * - `UnitComponent` reuses `resolveUnitByRef` (shared with
150
- * `IfcPhysicalSimpleQuantity.Unit` in `quantity-collect.ts`), NOT
151
- * `quantitySiScale`'s project-unit-fallback machinery: `IfcMeasureWithUnit`
152
- * is not an `IfcPhysicalQuantity`, `UnitComponent` is not `OPTIONAL`
153
- * within it, and there is no containing project unit to fall back to — the
154
- * reference resolves or it does not.
155
- */
156
- function extractUnitBasis(extractor, store, unitBasisRef) {
157
- if (unitBasisRef === undefined)
158
- return undefined;
159
- const ref = store.entityIndex.byId.get(unitBasisRef);
160
- if (!ref)
161
- return undefined;
162
- const entity = extractor.extractEntity(ref);
163
- if (!entity || entity.type.toUpperCase() !== 'IFCMEASUREWITHUNIT')
54
+ function appliedValue(value, reader, expressId) {
55
+ if (!reader.attributePresent(expressId, 2))
164
56
  return undefined;
165
- const a = entity.attributes || [];
166
- const unitRef = asRef(a[1]);
167
- const unit = unitRef !== undefined ? resolveUnitByRef(extractor, store.entityIndex, unitRef) : null;
168
- return {
169
- valueComponent: asAppliedValueNumber(a[0]),
170
- unitSymbol: unit?.resolved.symbol,
171
- unitSiScale: unit?.resolved.siScale,
172
- };
57
+ if (value === null || value === undefined)
58
+ return { Kind: 'Unsupported', Raw: value };
59
+ if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'string') {
60
+ const parsed = reader.decimalLexeme(expressId, 2);
61
+ return parsed === undefined
62
+ ? { Kind: 'Unsupported', Raw: value, InvalidNumber: true }
63
+ : { Kind: 'Typed', Type: value[0].toUpperCase(), Value: parsed };
64
+ }
65
+ const reference = reader.referenceLexeme(expressId, 2);
66
+ if (reference !== undefined && reader.get(reference)) {
67
+ return { Kind: 'Reference', expressId: reference };
68
+ }
69
+ return { Kind: 'Unsupported', Raw: value };
173
70
  }
174
- const MAX_COST_VALUE_DEPTH = 20;
175
- function extractCostValue(extractor, store, valueId, depth, visiting) {
176
- if (depth > MAX_COST_VALUE_DEPTH || visiting.has(valueId))
177
- return undefined;
178
- const ref = store.entityIndex.byId.get(valueId);
179
- if (!ref)
180
- return undefined;
181
- const entity = extractor.extractEntity(ref);
182
- if (!entity)
183
- return undefined;
184
- if (entity.type.toUpperCase() !== 'IFCCOSTVALUE')
185
- return undefined;
186
- const a = entity.attributes || [];
187
- visiting.add(valueId);
188
- const componentIds = asRefList(a[COST_VALUE_ATTR.Components]);
189
- const components = componentIds.length
190
- ? componentIds
191
- .map((id) => extractCostValue(extractor, store, id, depth + 1, visiting))
192
- .filter((c) => c !== undefined)
193
- : undefined;
194
- visiting.delete(valueId);
195
- return {
196
- name: asString(a[COST_VALUE_ATTR.Name]),
197
- description: asString(a[COST_VALUE_ATTR.Description]),
198
- appliedValue: asAppliedValueNumber(a[COST_VALUE_ATTR.AppliedValue]),
199
- unitBasis: extractUnitBasis(extractor, store, asRef(a[COST_VALUE_ATTR.UnitBasis])),
200
- applicableDate: asString(a[COST_VALUE_ATTR.ApplicableDate]),
201
- fixedUntilDate: asString(a[COST_VALUE_ATTR.FixedUntilDate]),
202
- category: asString(a[COST_VALUE_ATTR.Category]),
203
- condition: asString(a[COST_VALUE_ATTR.Condition]),
204
- arithmeticOperator: asEnum(a[COST_VALUE_ATTR.ArithmeticOperator]),
205
- ...(components && components.length ? { components } : {}),
206
- };
71
+ function quantityType(dimension) {
72
+ switch (dimension) {
73
+ case 'length': return QuantityType.Length;
74
+ case 'area': return QuantityType.Area;
75
+ case 'volume': return QuantityType.Volume;
76
+ case 'mass': return QuantityType.Weight;
77
+ case 'time': return QuantityType.Time;
78
+ case 'number': return QuantityType.Number;
79
+ default: return QuantityType.Count;
80
+ }
207
81
  }
208
- function extractCostValues(extractor, store, refs) {
209
- const ids = asRefList(refs);
210
- if (ids.length === 0)
82
+ function finiteCompatibilityNumber(value) {
83
+ if (value === undefined)
84
+ return undefined;
85
+ const converted = Number(value);
86
+ if (!Number.isFinite(converted))
211
87
  return undefined;
212
- const visiting = new Set();
213
- const values = ids
214
- .map((id) => extractCostValue(extractor, store, id, 0, visiting))
215
- .filter((v) => v !== undefined);
216
- return values.length ? values : undefined;
88
+ return converted !== 0 || isZeroCostNumericLexeme(value) ? converted : undefined;
217
89
  }
218
90
  /**
219
- * Extract all costing data from a parsed IFC store.
220
- *
221
- * Walks every IfcCostItem / IfcCostSchedule / IfcRelNests /
222
- * IfcRelAssignsToControl entity and assembles a connected CostExtraction.
91
+ * `IfcRelAssignsToControl` legitimately binds tasks, resources and actors
92
+ * to a control (assigning a task to a cost item is exactly this
93
+ * relationship, with the cost item as `RelatingControl`) — not just
94
+ * products. `productExpressIds`/`productGlobalIds` are documented as an
95
+ * `IfcProduct` view, so this discriminates the related object's type via
96
+ * the schema-derived inheritance chain (same approach used elsewhere in
97
+ * the parser for product/group/root classification) before it is
98
+ * admitted (#4877).
223
99
  */
224
- export function extractCostOnDemand(store) {
225
- if (!store.source?.length) {
226
- return { costSchedules: [], costItems: [], hasCost: false };
100
+ function isProductLike(reader, expressId) {
101
+ const type = reader.typeOf(expressId);
102
+ if (!type)
103
+ return false;
104
+ return getInheritanceChain(type).some((ancestor) => ancestor.toUpperCase() === 'IFCPRODUCT');
105
+ }
106
+ function pushInvalidList(reader, diagnostics, expressId, index, attribute, value) {
107
+ if (!reader.attributePresent(expressId, index))
108
+ return undefined;
109
+ const refs = reader.referenceListLexeme(expressId, index);
110
+ if (!refs || refs.length === 0 || !Array.isArray(value) || refs.length !== value.length) {
111
+ diagnostics.push({
112
+ Code: 'INVALID_LIST',
113
+ Message: `${attribute} on #${expressId} must contain at least one reference`,
114
+ Severity: 'warning',
115
+ expressId,
116
+ });
117
+ return [];
227
118
  }
228
- const byType = store.entityIndex.byType;
229
- const costItemIds = byType.get('IFCCOSTITEM') ?? [];
230
- const costScheduleIds = byType.get('IFCCOSTSCHEDULE') ?? [];
231
- const relNestsIds = byType.get('IFCRELNESTS') ?? [];
232
- const relAssignsControlIds = byType.get('IFCRELASSIGNSTOCONTROL') ?? [];
233
- if (costItemIds.length + costScheduleIds.length === 0) {
234
- return { costSchedules: [], costItems: [], hasCost: false };
119
+ return refs;
120
+ }
121
+ function extractSchedules(reader, schemaIs2x3) {
122
+ const result = [];
123
+ for (const expressId of reader.ids('IFCCOSTSCHEDULE')) {
124
+ const a = reader.get(expressId)?.attributes ?? [];
125
+ result.push(schemaIs2x3 ? {
126
+ expressId,
127
+ GlobalId: asString(a[0]) ?? '',
128
+ Name: asString(a[2]), Description: asString(a[3]), ObjectType: asString(a[4]),
129
+ SubmittedOn: dateTime2x3(reader, a[7]), Status: asString(a[8]),
130
+ UpdateDate: dateTime2x3(reader, a[10]), ID: asString(a[11]), PredefinedType: asEnum(a[12]),
131
+ globalId: asString(a[0]) ?? '', name: asString(a[2]) ?? '',
132
+ predefinedType: asEnum(a[12]), status: asString(a[8]),
133
+ submittedOn: dateTime2x3(reader, a[7]), updateDate: dateTime2x3(reader, a[10]),
134
+ costItemGlobalIds: [],
135
+ } : {
136
+ expressId,
137
+ GlobalId: asString(a[0]) ?? '',
138
+ Name: asString(a[2]), Description: asString(a[3]), ObjectType: asString(a[4]),
139
+ Identification: asString(a[5]), PredefinedType: asEnum(a[6]), Status: asString(a[7]),
140
+ SubmittedOn: scalarString(a[8]), UpdateDate: scalarString(a[9]),
141
+ globalId: asString(a[0]) ?? '', name: asString(a[2]) ?? '',
142
+ predefinedType: asEnum(a[6]), status: asString(a[7]),
143
+ submittedOn: scalarString(a[8]), updateDate: scalarString(a[9]),
144
+ costItemGlobalIds: [],
145
+ });
235
146
  }
236
- const extractor = new EntityExtractor(store.source);
237
- // IFC2X3's IfcCostItem has no attributes at all — schema-version-gated,
238
- // not inferred from a missing value (see module doc comment).
239
- const schemaIs2x3 = store.schemaVersion === 'IFC2X3';
240
- const costItemByExpressId = new Map();
241
- // Pass 1: base IfcCostItem records.
242
- for (const expressId of costItemIds) {
243
- const ref = store.entityIndex.byId.get(expressId);
244
- if (!ref)
245
- continue;
246
- const entity = extractor.extractEntity(ref);
147
+ return result;
148
+ }
149
+ function extractItems(reader, schemaIs2x3, diagnostics) {
150
+ return reader.ids('IFCCOSTITEM').map((expressId) => {
151
+ const a = reader.get(expressId)?.attributes ?? [];
152
+ const base = {
153
+ expressId, GlobalId: asString(a[0]) ?? '', Name: asString(a[2]),
154
+ Description: asString(a[3]), ObjectType: asString(a[4]),
155
+ globalId: asString(a[0]) ?? '', name: asString(a[2]) ?? '',
156
+ childGlobalIds: [], productExpressIds: [], productGlobalIds: [],
157
+ controllingScheduleGlobalIds: [],
158
+ };
159
+ if (schemaIs2x3)
160
+ return base;
161
+ base.Identification = asString(a[5]);
162
+ base.PredefinedType = asEnum(a[6]);
163
+ base.predefinedType = base.PredefinedType;
164
+ base.CostValues = pushInvalidList(reader, diagnostics, expressId, 7, 'CostValues', a[7]);
165
+ base.CostQuantities = pushInvalidList(reader, diagnostics, expressId, 8, 'CostQuantities', a[8]);
166
+ return base;
167
+ });
168
+ }
169
+ function extractValues(reader, schemaIs2x3, diagnostics) {
170
+ const ids = new Set([...reader.ids('IFCCOSTVALUE'), ...reader.ids('IFCAPPLIEDVALUE')]);
171
+ const values = [];
172
+ for (const expressId of ids) {
173
+ const entity = reader.get(expressId);
247
174
  if (!entity)
248
175
  continue;
249
- const a = entity.attributes || [];
250
- const globalId = asString(a[COST_ITEM_ATTR.GlobalId]) ?? '';
251
- const item = {
176
+ const a = entity.attributes ?? [];
177
+ const AppliedValue = appliedValue(a[2], reader, expressId);
178
+ const ApplicableDate = schemaIs2x3 ? dateTime2x3(reader, a[4]) : scalarString(a[4]);
179
+ const FixedUntilDate = schemaIs2x3 ? dateTime2x3(reader, a[5]) : scalarString(a[5]);
180
+ const Condition = asString(a[7]);
181
+ const InvalidCondition = reader.attributePresent(expressId, 7) && Condition === undefined;
182
+ const UnitBasis = reader.referenceLexeme(expressId, 3);
183
+ const InvalidUnitBasis = reader.attributePresent(expressId, 3) && UnitBasis === undefined;
184
+ const base = {
252
185
  expressId,
253
- globalId,
254
- name: asString(a[COST_ITEM_ATTR.Name]) ?? '',
255
- predefinedType: schemaIs2x3 ? undefined : asEnum(a[COST_ITEM_ATTR.PredefinedType]),
256
- costValues: schemaIs2x3
257
- ? undefined
258
- : extractCostValues(extractor, store, a[COST_ITEM_ATTR.CostValues]),
259
- costQuantities: schemaIs2x3
260
- ? undefined
261
- : (() => {
262
- const qtys = collectQuantitiesFromRefs(store, extractor, a[COST_ITEM_ATTR.CostQuantities]);
263
- return qtys.length ? qtys : undefined;
264
- })(),
265
- childGlobalIds: [],
266
- productExpressIds: [],
267
- productGlobalIds: [],
268
- controllingScheduleGlobalIds: [],
186
+ Type: entity.type.toUpperCase() === 'IFCCOSTVALUE' ? 'IfcCostValue' : 'IfcAppliedValue',
187
+ Name: asString(a[0]), Description: asString(a[1]), AppliedValue,
188
+ UnitBasis, InvalidUnitBasis: InvalidUnitBasis || undefined, ApplicableDate, FixedUntilDate,
189
+ InvalidCondition: InvalidCondition || undefined,
190
+ name: asString(a[0]), description: asString(a[1]),
191
+ appliedValue: AppliedValue?.Kind === 'Typed'
192
+ ? finiteCompatibilityNumber(AppliedValue.Value)
193
+ : undefined,
194
+ applicableDate: ApplicableDate, fixedUntilDate: FixedUntilDate,
269
195
  };
270
- costItemByExpressId.set(expressId, item);
196
+ if (schemaIs2x3) {
197
+ base.CostType = asString(a[6]);
198
+ base.Condition = Condition;
199
+ base.condition = base.Condition;
200
+ }
201
+ else {
202
+ base.Category = asString(a[6]);
203
+ base.Condition = Condition;
204
+ base.ArithmeticOperator = asEnum(a[8]);
205
+ base.Components = pushInvalidList(reader, diagnostics, expressId, 9, 'Components', a[9]);
206
+ base.category = base.Category;
207
+ base.condition = base.Condition;
208
+ base.arithmeticOperator = base.ArithmeticOperator;
209
+ }
210
+ values.push(base);
271
211
  }
272
- // Pass 2: IfcRelNests — cost item breakdown hierarchy.
273
- for (const relId of relNestsIds) {
274
- const ref = store.entityIndex.byId.get(relId);
275
- if (!ref)
276
- continue;
277
- const entity = extractor.extractEntity(ref);
278
- if (!entity)
279
- continue;
280
- const a = entity.attributes || [];
281
- const parent = asRef(a[REL_NESTS_ATTR.RelatingObject]);
282
- const children = asRefList(a[REL_NESTS_ATTR.RelatedObjects]);
283
- if (parent === undefined)
284
- continue;
285
- const parentItem = costItemByExpressId.get(parent);
286
- if (!parentItem)
287
- continue; // nesting over non-cost-item entities — ignore
288
- for (const childId of children) {
289
- const childItem = costItemByExpressId.get(childId);
290
- if (!childItem)
291
- continue;
292
- parentItem.childGlobalIds.push(childItem.globalId);
293
- if (!childItem.parentGlobalId) {
294
- childItem.parentGlobalId = parentItem.globalId;
295
- }
212
+ return values;
213
+ }
214
+ /** Extract a schema-aware, read-only IFC cost graph from a parsed store. */
215
+ export function extractCostOnDemand(store, options) {
216
+ const schema = store.schemaVersion;
217
+ const diagnostics = [];
218
+ const empty = () => ({
219
+ SchemaVersion: schema, CostSchedules: [], CostItems: [], CostValues: [],
220
+ CostQuantities: [], Units: [], MeasuresWithUnit: [], ProjectUnits: {}, Relationships: [], Diagnostics: diagnostics,
221
+ HasCostData: false, costSchedules: [], costItems: [], hasCost: false,
222
+ });
223
+ if (!store.source?.length)
224
+ return empty();
225
+ const reader = new CostEntityReader(store, options?.overlay, diagnostics);
226
+ const hasCost = reader.ids('IFCCOSTSCHEDULE').length + reader.ids('IFCCOSTITEM').length +
227
+ reader.ids('IFCCOSTVALUE').length + reader.ids('IFCAPPLIEDVALUE').length > 0;
228
+ if (!hasCost)
229
+ return empty();
230
+ if (schema === 'IFC5') {
231
+ diagnostics.push({ Code: 'UNSUPPORTED_SCHEMA', Message: 'IFC5 cost extraction is not supported', Severity: 'error' });
232
+ return { ...empty(), HasCostData: true };
233
+ }
234
+ const schemaIs2x3 = schema === 'IFC2X3';
235
+ if (schemaIs2x3)
236
+ diagnostics.push({
237
+ Code: 'IFC2X3_PARTIAL_READ',
238
+ Message: 'IFC2X3 cost metadata and relationships are preserved, but values are not evaluated',
239
+ Severity: 'warning',
240
+ });
241
+ const CostSchedules = extractSchedules(reader, schemaIs2x3);
242
+ const CostItems = extractItems(reader, schemaIs2x3, diagnostics);
243
+ const CostValues = extractValues(reader, schemaIs2x3, diagnostics);
244
+ const itemIds = new Set(CostItems.map(item => item.expressId));
245
+ const scheduleIds = new Set(CostSchedules.map(schedule => schedule.expressId));
246
+ const valueIds = new Set(CostValues.flatMap(value => value.expressId === undefined ? [] : [value.expressId]));
247
+ const Relationships = extractCostRelationships(reader, itemIds, scheduleIds, valueIds);
248
+ const quantityMap = new Map();
249
+ extractCostQuantities(reader, CostItems.flatMap(item => item.CostQuantities ?? []), quantityMap, diagnostics);
250
+ const unitResolver = new CostUnitResolver(reader, diagnostics);
251
+ for (const quantity of quantityMap.values()) {
252
+ if (quantity.Unit !== undefined)
253
+ unitResolver.resolve(quantity.Unit);
254
+ }
255
+ for (const value of CostValues) {
256
+ if (value.UnitBasis !== undefined) {
257
+ const basis = unitResolver.resolveMeasureWithUnit(value.UnitBasis);
258
+ const compatibility = unitResolver.compatibilityMeasureWithUnit(value.UnitBasis, basis);
259
+ const valueComponent = finiteCompatibilityNumber(compatibility?.Value);
260
+ const unitSiScale = finiteCompatibilityNumber(compatibility?.Unit?.Scale);
261
+ value.unitBasis = compatibility ? {
262
+ valueComponent,
263
+ unitSymbol: compatibility.Unit?.Scale === undefined ? undefined : compatibility.Unit.Symbol,
264
+ unitSiScale,
265
+ } : undefined;
266
+ }
267
+ if (value.AppliedValue?.Kind === 'Reference' && reader.typeOf(value.AppliedValue.expressId) === 'IFCMEASUREWITHUNIT') {
268
+ unitResolver.resolveMeasureWithUnit(value.AppliedValue.expressId);
296
269
  }
297
270
  }
298
- // Pass 3: extract cost schedules.
299
- const costSchedules = [];
300
- const scheduleByExpressId = new Map();
301
- for (const expressId of costScheduleIds) {
302
- const ref = store.entityIndex.byId.get(expressId);
303
- if (!ref)
304
- continue;
305
- const entity = extractor.extractEntity(ref);
306
- if (!entity)
307
- continue;
308
- const a = entity.attributes || [];
309
- const globalId = asString(a[COST_SCHEDULE_ATTR.GlobalId]) ?? '';
310
- const info = {
311
- expressId,
312
- globalId,
313
- name: asString(a[COST_SCHEDULE_ATTR.Name]) ?? '',
314
- // IFC2X3 lays the schedule out differently (see cost-schedule-2x3.ts).
315
- predefinedType: asEnum(a[schemaIs2x3 ? COST_SCHEDULE_ATTR_2X3.PredefinedType : COST_SCHEDULE_ATTR.PredefinedType]),
316
- status: asString(a[schemaIs2x3 ? COST_SCHEDULE_ATTR_2X3.Status : COST_SCHEDULE_ATTR.Status]),
317
- submittedOn: schemaIs2x3
318
- ? resolveDateTimeSelect2x3(extractor, store, a[COST_SCHEDULE_ATTR_2X3.SubmittedOn])
319
- : asString(a[COST_SCHEDULE_ATTR.SubmittedOn]),
320
- updateDate: schemaIs2x3
321
- ? resolveDateTimeSelect2x3(extractor, store, a[COST_SCHEDULE_ATTR_2X3.UpdateDate])
322
- : asString(a[COST_SCHEDULE_ATTR.UpdateDate]),
323
- costItemGlobalIds: [],
324
- };
325
- costSchedules.push(info);
326
- scheduleByExpressId.set(expressId, info);
271
+ diagnoseCostGraphs(Relationships, CostValues, itemIds, diagnostics);
272
+ const schedules = new Map(CostSchedules.map(schedule => [schedule.expressId, schedule]));
273
+ const items = new Map(CostItems.map(item => [item.expressId, item]));
274
+ const values = new Map(CostValues.map(value => [value.expressId, value]));
275
+ const legacyComponents = compatibilityTreeBuilder(values, diagnostics);
276
+ const quantities = new Map([...quantityMap.values()].map(quantity => [quantity.expressId, quantity]));
277
+ for (const value of CostValues) {
278
+ for (const componentId of value.Components ?? []) {
279
+ if (!values.has(componentId))
280
+ diagnostics.push({
281
+ Code: 'MISSING_REFERENCE',
282
+ Message: `IfcAppliedValue component #${componentId} cannot be resolved`,
283
+ Severity: 'error', expressId: value.expressId, RelatedExpressId: componentId,
284
+ });
285
+ }
327
286
  }
328
- // Pass 4: IfcRelAssignsToControl — schedule->cost-item assignment AND
329
- // cost-item->object (product) assignment, told apart by what
330
- // RelatingControl resolves to (see module doc comment).
331
- for (const relId of relAssignsControlIds) {
332
- const ref = store.entityIndex.byId.get(relId);
333
- if (!ref)
334
- continue;
335
- const entity = extractor.extractEntity(ref);
336
- if (!entity)
337
- continue;
338
- const a = entity.attributes || [];
339
- const controlId = asRef(a[REL_ASSIGNS_TO_CONTROL_ATTR.RelatingControl]);
340
- if (controlId === undefined)
341
- continue;
342
- const relatedIds = asRefList(a[REL_ASSIGNS_TO_CONTROL_ATTR.RelatedObjects]);
343
- const schedule = scheduleByExpressId.get(controlId);
344
- if (schedule) {
345
- for (const objId of relatedIds) {
346
- const item = costItemByExpressId.get(objId);
347
- if (!item)
287
+ const compatibilityBuilt = new Set();
288
+ for (const item of CostItems) {
289
+ for (const valueId of item.CostValues ?? []) {
290
+ const value = values.get(valueId);
291
+ if (!value)
292
+ diagnostics.push({
293
+ Code: 'MISSING_REFERENCE',
294
+ Message: `IfcCostValue #${valueId} cannot be resolved`,
295
+ Severity: 'error', expressId: item.expressId, RelatedExpressId: valueId,
296
+ });
297
+ else if (value.Type !== 'IfcCostValue')
298
+ diagnostics.push({
299
+ Code: 'INVALID_LIST',
300
+ Message: `CostValues on IfcCostItem #${item.expressId} must reference IfcCostValue`,
301
+ Severity: 'error', expressId: item.expressId, RelatedExpressId: valueId,
302
+ });
303
+ }
304
+ item.costValues = item.CostValues
305
+ ?.map(id => {
306
+ const value = values.get(id);
307
+ if (value && !compatibilityBuilt.has(id)) {
308
+ value.components = legacyComponents(id);
309
+ compatibilityBuilt.add(id);
310
+ }
311
+ return value;
312
+ })
313
+ .filter((entry) => entry !== undefined);
314
+ item.costQuantities = item.CostQuantities
315
+ ?.map(id => quantities.get(id))
316
+ .filter((entry) => entry !== undefined && costQuantityExactValue(entry) !== undefined)
317
+ .flatMap(quantity => {
318
+ const value = finiteCompatibilityNumber(costQuantityExactValue(quantity));
319
+ if (value === undefined)
320
+ return [];
321
+ const unit = quantity.Unit === undefined ? undefined : unitResolver.Units.get(quantity.Unit);
322
+ const explicitUnitSiScale = finiteCompatibilityNumber(unit?.Scale);
323
+ return [{
324
+ name: quantity.Name ?? '',
325
+ type: quantityType(quantity.Dimension),
326
+ value,
327
+ ...(explicitUnitSiScale !== undefined ? { explicitUnitSiScale } : {}),
328
+ }];
329
+ });
330
+ if (item.costQuantities?.length === 0)
331
+ item.costQuantities = undefined;
332
+ }
333
+ for (const relation of Relationships) {
334
+ if (relation.Type === 'IfcRelNests' && relation.RelatingObject !== undefined) {
335
+ const parent = items.get(relation.RelatingObject);
336
+ if (!parent)
337
+ continue;
338
+ for (const childId of relation.RelatedObjects ?? []) {
339
+ const child = items.get(childId);
340
+ if (!child)
348
341
  continue;
349
- schedule.costItemGlobalIds.push(item.globalId);
350
- item.controllingScheduleGlobalIds.push(schedule.globalId);
342
+ parent.childGlobalIds.push(child.GlobalId ?? '');
343
+ child.parentGlobalId ??= parent.GlobalId ?? '';
344
+ }
345
+ }
346
+ else if ((relation.Type === 'IfcRelAssignsToControl' || relation.Type === 'IfcRelSchedulesCostItems') &&
347
+ relation.RelatingControl !== undefined) {
348
+ const schedule = schedules.get(relation.RelatingControl);
349
+ const item = items.get(relation.RelatingControl);
350
+ for (const relatedId of relation.RelatedObjects ?? []) {
351
+ const relatedItem = items.get(relatedId);
352
+ if (schedule && relatedItem) {
353
+ schedule.costItemGlobalIds.push(relatedItem.GlobalId ?? '');
354
+ relatedItem.controllingScheduleGlobalIds.push(schedule.GlobalId ?? '');
355
+ }
356
+ else if (item && isProductLike(reader, relatedId)) {
357
+ item.productExpressIds.push(relatedId);
358
+ item.productGlobalIds.push(store.entities?.getGlobalId?.(relatedId) ?? '');
359
+ }
351
360
  }
352
- continue;
353
361
  }
354
- const costItem = costItemByExpressId.get(controlId);
355
- if (costItem) {
356
- for (const productId of relatedIds) {
357
- const gid = store.entities?.getGlobalId?.(productId) ?? undefined;
358
- costItem.productExpressIds.push(productId);
359
- costItem.productGlobalIds.push(gid ?? '');
362
+ else if (relation.Type === 'IfcRelAssignsToProduct' && relation.RelatingProduct !== undefined) {
363
+ for (const relatedId of relation.RelatedObjects ?? []) {
364
+ const item = items.get(relatedId);
365
+ if (!item)
366
+ continue;
367
+ item.productExpressIds.push(relation.RelatingProduct);
368
+ item.productGlobalIds.push(store.entities?.getGlobalId?.(relation.RelatingProduct) ?? '');
360
369
  }
361
370
  }
362
371
  }
363
372
  return {
364
- costSchedules,
365
- costItems: Array.from(costItemByExpressId.values()),
366
- hasCost: true,
373
+ SchemaVersion: schema, CostSchedules, CostItems, CostValues,
374
+ CostQuantities: [...quantityMap.values()], Units: [...unitResolver.Units.values()],
375
+ MeasuresWithUnit: [...unitResolver.MeasuresWithUnit.values()],
376
+ ProjectUnits: Object.fromEntries([...unitResolver.ProjectUnits.entries()].map(([dimension, unit]) => [dimension, unit.expressId])),
377
+ Relationships, Diagnostics: diagnostics, HasCostData: true, Currency: unitResolver.Currency,
378
+ costSchedules: CostSchedules, costItems: CostItems, hasCost: true,
367
379
  };
368
380
  }
369
381
  //# sourceMappingURL=cost-extractor.js.map