altium-toolkit 1.0.7 → 1.0.9
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.
- package/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds consumer-facing effective views for project variants.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectVariantViewBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Applies one project variant to bundle-level BOM, PnP, component, and net
|
|
11
|
+
* collections.
|
|
12
|
+
* @param {object} bundle Project design bundle.
|
|
13
|
+
* @param {{ variantName?: string }} options Variant selection options.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(bundle, options = {}) {
|
|
17
|
+
const variant = ProjectVariantViewBuilder.#findVariant(
|
|
18
|
+
bundle,
|
|
19
|
+
options.variantName
|
|
20
|
+
)
|
|
21
|
+
const dnp = new Set(variant?.dnp || [])
|
|
22
|
+
const parameterOverrides = variant?.parameterOverrides || {}
|
|
23
|
+
const alternateFitted = variant?.alternateFitted || {}
|
|
24
|
+
const annotations = bundle?.annotations?.bySourceDesignator || {}
|
|
25
|
+
const includeVariantDetails =
|
|
26
|
+
ProjectVariantViewBuilder.#hasKeys(alternateFitted) ||
|
|
27
|
+
ProjectVariantViewBuilder.#hasKeys(annotations)
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: variant?.description || options.variantName || '',
|
|
31
|
+
uniqueId: variant?.uniqueId || '',
|
|
32
|
+
dnp: [...dnp],
|
|
33
|
+
parameterOverrides,
|
|
34
|
+
...(includeVariantDetails ? { alternateFitted } : {}),
|
|
35
|
+
bom: ProjectVariantViewBuilder.#applyBomVariant(
|
|
36
|
+
bundle?.bom || [],
|
|
37
|
+
dnp,
|
|
38
|
+
parameterOverrides,
|
|
39
|
+
alternateFitted,
|
|
40
|
+
annotations,
|
|
41
|
+
includeVariantDetails
|
|
42
|
+
),
|
|
43
|
+
pnp: {
|
|
44
|
+
...(bundle?.pnp || {}),
|
|
45
|
+
entries: (bundle?.pnp?.entries || [])
|
|
46
|
+
.filter((entry) => !dnp.has(entry.designator))
|
|
47
|
+
.map((entry) =>
|
|
48
|
+
ProjectVariantViewBuilder.#applyDesignatorAnnotation(
|
|
49
|
+
entry,
|
|
50
|
+
annotations
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
nets: ProjectVariantViewBuilder.#applyNetVariant(
|
|
55
|
+
bundle?.nets || [],
|
|
56
|
+
dnp,
|
|
57
|
+
annotations
|
|
58
|
+
),
|
|
59
|
+
components: (bundle?.components || []).map((component) =>
|
|
60
|
+
ProjectVariantViewBuilder.#applyComponentVariant(
|
|
61
|
+
component,
|
|
62
|
+
dnp,
|
|
63
|
+
parameterOverrides,
|
|
64
|
+
alternateFitted,
|
|
65
|
+
annotations,
|
|
66
|
+
includeVariantDetails
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Finds the requested variant or the current project variant.
|
|
74
|
+
* @param {object} bundle Project design bundle.
|
|
75
|
+
* @param {string | undefined} variantName Requested variant name.
|
|
76
|
+
* @returns {object | null}
|
|
77
|
+
*/
|
|
78
|
+
static #findVariant(bundle, variantName) {
|
|
79
|
+
const variants = bundle?.variants || bundle?.project?.variants || []
|
|
80
|
+
const requested =
|
|
81
|
+
variantName || bundle?.project?.design?.CurrentVariant || ''
|
|
82
|
+
if (!requested) {
|
|
83
|
+
return variants.find((variant) => variant.isCurrent) || null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lookup = requested.toLowerCase()
|
|
87
|
+
return (
|
|
88
|
+
variants.find(
|
|
89
|
+
(variant) =>
|
|
90
|
+
String(variant.description || '').toLowerCase() ===
|
|
91
|
+
lookup ||
|
|
92
|
+
String(variant.uniqueId || '').toLowerCase() === lookup
|
|
93
|
+
) ||
|
|
94
|
+
variants.find((variant) => variant.isCurrent) ||
|
|
95
|
+
null
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Applies DNP and parameter overrides to BOM rows.
|
|
101
|
+
* @param {object[]} bom BOM rows.
|
|
102
|
+
* @param {Set<string>} dnp DNP designators.
|
|
103
|
+
* @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
|
|
104
|
+
* @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
|
|
105
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
106
|
+
* @param {boolean} includeVariantDetails Whether to expose alternate metadata.
|
|
107
|
+
* @returns {object[]}
|
|
108
|
+
*/
|
|
109
|
+
static #applyBomVariant(
|
|
110
|
+
bom,
|
|
111
|
+
dnp,
|
|
112
|
+
parameterOverrides,
|
|
113
|
+
alternateFitted,
|
|
114
|
+
annotations,
|
|
115
|
+
includeVariantDetails
|
|
116
|
+
) {
|
|
117
|
+
return (bom || [])
|
|
118
|
+
.flatMap((row) =>
|
|
119
|
+
(row.designators || []).map((designator) =>
|
|
120
|
+
ProjectVariantViewBuilder.#applyBomDesignatorVariant(
|
|
121
|
+
row,
|
|
122
|
+
designator,
|
|
123
|
+
parameterOverrides,
|
|
124
|
+
alternateFitted,
|
|
125
|
+
annotations,
|
|
126
|
+
includeVariantDetails
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
.filter(
|
|
131
|
+
(row) => !dnp.has(row.sourceDesignator || row.designators[0])
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Applies one designator's parameter overrides to a BOM row.
|
|
137
|
+
* @param {object} row Source BOM row.
|
|
138
|
+
* @param {string} designator Designator.
|
|
139
|
+
* @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
|
|
140
|
+
* @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
|
|
141
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
142
|
+
* @param {boolean} includeVariantDetails Whether to expose alternate metadata.
|
|
143
|
+
* @returns {object}
|
|
144
|
+
*/
|
|
145
|
+
static #applyBomDesignatorVariant(
|
|
146
|
+
row,
|
|
147
|
+
designator,
|
|
148
|
+
parameterOverrides,
|
|
149
|
+
alternateFitted,
|
|
150
|
+
annotations,
|
|
151
|
+
includeVariantDetails
|
|
152
|
+
) {
|
|
153
|
+
const parameters = parameterOverrides[designator] || {}
|
|
154
|
+
const alternate = alternateFitted[designator] || null
|
|
155
|
+
const value =
|
|
156
|
+
parameters.Comment ||
|
|
157
|
+
parameters.Value ||
|
|
158
|
+
alternate?.comment ||
|
|
159
|
+
alternate?.description ||
|
|
160
|
+
row.value
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...row,
|
|
164
|
+
designators: [
|
|
165
|
+
ProjectVariantViewBuilder.#compiledDesignator(
|
|
166
|
+
designator,
|
|
167
|
+
annotations
|
|
168
|
+
)
|
|
169
|
+
],
|
|
170
|
+
...(includeVariantDetails ? { sourceDesignator: designator } : {}),
|
|
171
|
+
quantity: 1,
|
|
172
|
+
value,
|
|
173
|
+
source: alternate?.libReference || row.source,
|
|
174
|
+
pattern: alternate?.footprint || row.pattern,
|
|
175
|
+
parameters,
|
|
176
|
+
...(includeVariantDetails
|
|
177
|
+
? { alternateFitted: alternate || null }
|
|
178
|
+
: {})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Applies DNP filtering metadata to normalized nets.
|
|
184
|
+
* @param {object[]} nets Bundle nets.
|
|
185
|
+
* @param {Set<string>} dnp DNP designators.
|
|
186
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
187
|
+
* @returns {object[]}
|
|
188
|
+
*/
|
|
189
|
+
static #applyNetVariant(nets, dnp, annotations) {
|
|
190
|
+
return (nets || []).map((net) => {
|
|
191
|
+
const excludedDesignators = []
|
|
192
|
+
const pins = (net.pins || [])
|
|
193
|
+
.filter((pin) => {
|
|
194
|
+
const designator =
|
|
195
|
+
pin.componentDesignator || pin.ownerIndex || ''
|
|
196
|
+
if (dnp.has(designator)) {
|
|
197
|
+
excludedDesignators.push(designator)
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
return true
|
|
201
|
+
})
|
|
202
|
+
.map((pin) =>
|
|
203
|
+
ProjectVariantViewBuilder.#applyPinAnnotation(
|
|
204
|
+
pin,
|
|
205
|
+
annotations
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
...net,
|
|
211
|
+
pins,
|
|
212
|
+
excludedDesignators:
|
|
213
|
+
ProjectVariantViewBuilder.#dedupe(excludedDesignators)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Applies DNP and parameter metadata to one component entry.
|
|
220
|
+
* @param {object} component Bundle component.
|
|
221
|
+
* @param {Set<string>} dnp DNP designators.
|
|
222
|
+
* @param {Record<string, Record<string, string>>} parameterOverrides Overrides by designator.
|
|
223
|
+
* @param {Record<string, object>} alternateFitted Alternate fitted rows by designator.
|
|
224
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
225
|
+
* @param {boolean} includeVariantDetails Whether to expose alternate metadata.
|
|
226
|
+
* @returns {object}
|
|
227
|
+
*/
|
|
228
|
+
static #applyComponentVariant(
|
|
229
|
+
component,
|
|
230
|
+
dnp,
|
|
231
|
+
parameterOverrides,
|
|
232
|
+
alternateFitted,
|
|
233
|
+
annotations,
|
|
234
|
+
includeVariantDetails
|
|
235
|
+
) {
|
|
236
|
+
const alternate = alternateFitted[component.designator] || null
|
|
237
|
+
return {
|
|
238
|
+
designator: ProjectVariantViewBuilder.#compiledDesignator(
|
|
239
|
+
component.designator,
|
|
240
|
+
annotations
|
|
241
|
+
),
|
|
242
|
+
...(includeVariantDetails
|
|
243
|
+
? { sourceDesignator: component.designator }
|
|
244
|
+
: {}),
|
|
245
|
+
schematic: component.schematic,
|
|
246
|
+
pcb: component.pcb,
|
|
247
|
+
dnp: dnp.has(component.designator),
|
|
248
|
+
parameters: parameterOverrides[component.designator] || {},
|
|
249
|
+
...(includeVariantDetails
|
|
250
|
+
? { alternateFitted: alternate || null }
|
|
251
|
+
: {})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Applies an annotation row to one designator-bearing object.
|
|
257
|
+
* @param {object} entry Source entry.
|
|
258
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
259
|
+
* @returns {object}
|
|
260
|
+
*/
|
|
261
|
+
static #applyDesignatorAnnotation(entry, annotations) {
|
|
262
|
+
const compiledDesignator =
|
|
263
|
+
ProjectVariantViewBuilder.#compiledDesignator(
|
|
264
|
+
entry.designator,
|
|
265
|
+
annotations
|
|
266
|
+
)
|
|
267
|
+
return compiledDesignator === entry.designator
|
|
268
|
+
? entry
|
|
269
|
+
: {
|
|
270
|
+
...entry,
|
|
271
|
+
sourceDesignator: entry.designator,
|
|
272
|
+
designator: compiledDesignator
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Applies annotation mapping to one net pin.
|
|
278
|
+
* @param {object} pin Net pin.
|
|
279
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
280
|
+
* @returns {object}
|
|
281
|
+
*/
|
|
282
|
+
static #applyPinAnnotation(pin, annotations) {
|
|
283
|
+
const ownerIndex = String(pin?.ownerIndex || '')
|
|
284
|
+
const componentDesignator = String(pin?.componentDesignator || '')
|
|
285
|
+
const sourceDesignator = componentDesignator || ownerIndex
|
|
286
|
+
const compiledDesignator =
|
|
287
|
+
ProjectVariantViewBuilder.#compiledDesignator(
|
|
288
|
+
sourceDesignator,
|
|
289
|
+
annotations
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if (compiledDesignator === sourceDesignator) {
|
|
293
|
+
return pin
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
...pin,
|
|
298
|
+
sourceDesignator,
|
|
299
|
+
...(ownerIndex ? { ownerIndex: compiledDesignator } : {}),
|
|
300
|
+
...(componentDesignator
|
|
301
|
+
? { componentDesignator: compiledDesignator }
|
|
302
|
+
: {})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Resolves a compiled designator for a source designator.
|
|
308
|
+
* @param {string} designator Source designator.
|
|
309
|
+
* @param {Record<string, object>} annotations Annotation rows by source designator.
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
312
|
+
static #compiledDesignator(designator, annotations) {
|
|
313
|
+
const key = String(designator || '').trim()
|
|
314
|
+
return String(annotations?.[key]?.compiledDesignator || key)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Checks whether an object has enumerable keys.
|
|
319
|
+
* @param {object | undefined} value Candidate object.
|
|
320
|
+
* @returns {boolean}
|
|
321
|
+
*/
|
|
322
|
+
static #hasKeys(value) {
|
|
323
|
+
return Object.keys(value || {}).length > 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Deduplicates values while preserving order.
|
|
328
|
+
* @param {unknown[]} values Candidate values.
|
|
329
|
+
* @returns {unknown[]}
|
|
330
|
+
*/
|
|
331
|
+
static #dedupe(values) {
|
|
332
|
+
return [...new Set((values || []).filter(Boolean))]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getField, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a read-only schematic component library/model binding layer.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicBindingProvenanceParser {
|
|
13
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.bindings.a1'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses component binding status from component and implementation rows.
|
|
17
|
+
* @param {object[]} records Schematic records.
|
|
18
|
+
* @param {object | null} implementations Implementation read model.
|
|
19
|
+
* @returns {object | null}
|
|
20
|
+
*/
|
|
21
|
+
static parse(records, implementations) {
|
|
22
|
+
const componentRows =
|
|
23
|
+
SchematicBindingProvenanceParser.#componentRows(records)
|
|
24
|
+
|
|
25
|
+
if (!componentRows.length) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const implementationsByComponent =
|
|
30
|
+
SchematicBindingProvenanceParser.#implementationsByComponent(
|
|
31
|
+
implementations
|
|
32
|
+
)
|
|
33
|
+
const components = componentRows.map((component) =>
|
|
34
|
+
SchematicBindingProvenanceParser.#componentBinding(
|
|
35
|
+
component,
|
|
36
|
+
implementationsByComponent.get(component.componentKey) || []
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
schema: SchematicBindingProvenanceParser.SCHEMA_ID,
|
|
42
|
+
summary: SchematicBindingProvenanceParser.#summary(components),
|
|
43
|
+
components
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Builds normalized component identity rows.
|
|
49
|
+
* @param {object[]} records Schematic records.
|
|
50
|
+
* @returns {object[]}
|
|
51
|
+
*/
|
|
52
|
+
static #componentRows(records) {
|
|
53
|
+
return (records || [])
|
|
54
|
+
.filter((record) => getField(record.fields, 'RECORD') === '1')
|
|
55
|
+
.map((record) => {
|
|
56
|
+
const indexInSheet = parseNumericField(
|
|
57
|
+
record.fields,
|
|
58
|
+
'IndexInSheet'
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
componentKey:
|
|
62
|
+
'schematic-component-' +
|
|
63
|
+
String(indexInSheet ?? record.recordIndex ?? 0),
|
|
64
|
+
recordKey:
|
|
65
|
+
'schematic-record-' + String(record.recordIndex ?? 0),
|
|
66
|
+
uniqueId: getField(record.fields, 'UniqueID'),
|
|
67
|
+
libReference:
|
|
68
|
+
getField(record.fields, 'LibReference') ||
|
|
69
|
+
getField(record.fields, 'DesignItemId')
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Groups implementation rows by owner component key.
|
|
76
|
+
* @param {object | null} implementations Implementation read model.
|
|
77
|
+
* @returns {Map<string, object[]>}
|
|
78
|
+
*/
|
|
79
|
+
static #implementationsByComponent(implementations) {
|
|
80
|
+
const grouped = new Map()
|
|
81
|
+
|
|
82
|
+
for (const implementation of implementations?.implementations || []) {
|
|
83
|
+
const key = implementation.ownerComponentKey || ''
|
|
84
|
+
if (!key) continue
|
|
85
|
+
if (!grouped.has(key)) grouped.set(key, [])
|
|
86
|
+
grouped.get(key).push(implementation)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return grouped
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds one component binding row.
|
|
94
|
+
* @param {object} component Component row.
|
|
95
|
+
* @param {object[]} linkedImplementations Linked implementation rows.
|
|
96
|
+
* @returns {object}
|
|
97
|
+
*/
|
|
98
|
+
static #componentBinding(component, linkedImplementations) {
|
|
99
|
+
const status = SchematicBindingProvenanceParser.#status(
|
|
100
|
+
component,
|
|
101
|
+
linkedImplementations
|
|
102
|
+
)
|
|
103
|
+
const reasons = SchematicBindingProvenanceParser.#reasons(
|
|
104
|
+
status,
|
|
105
|
+
component,
|
|
106
|
+
linkedImplementations
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
componentKey: component.componentKey,
|
|
111
|
+
recordKey: component.recordKey,
|
|
112
|
+
uniqueId: component.uniqueId,
|
|
113
|
+
libReference: component.libReference,
|
|
114
|
+
status,
|
|
115
|
+
implementationKeys: linkedImplementations.map(
|
|
116
|
+
(implementation) => implementation.key
|
|
117
|
+
),
|
|
118
|
+
targetLibraries: SchematicBindingProvenanceParser.#targetLibraries(
|
|
119
|
+
linkedImplementations
|
|
120
|
+
),
|
|
121
|
+
reasons
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Classifies component binding status.
|
|
127
|
+
* @param {object} component Component row.
|
|
128
|
+
* @param {object[]} linkedImplementations Linked implementation rows.
|
|
129
|
+
* @returns {'resolved' | 'unresolved' | 'stale' | 'external'}
|
|
130
|
+
*/
|
|
131
|
+
static #status(component, linkedImplementations) {
|
|
132
|
+
if (linkedImplementations.length) {
|
|
133
|
+
const current =
|
|
134
|
+
linkedImplementations.find(
|
|
135
|
+
(implementation) => implementation.isCurrent
|
|
136
|
+
) || linkedImplementations[0]
|
|
137
|
+
return current.modelName &&
|
|
138
|
+
(current.targetLibraries || []).length > 0
|
|
139
|
+
? 'resolved'
|
|
140
|
+
: 'stale'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return component.libReference ? 'external' : 'unresolved'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Explains non-resolved status values.
|
|
148
|
+
* @param {string} status Binding status.
|
|
149
|
+
* @param {object} component Component row.
|
|
150
|
+
* @param {object[]} linkedImplementations Linked implementation rows.
|
|
151
|
+
* @returns {string[]}
|
|
152
|
+
*/
|
|
153
|
+
static #reasons(status, component, linkedImplementations) {
|
|
154
|
+
if (status === 'resolved') return []
|
|
155
|
+
if (status === 'external') {
|
|
156
|
+
return ['component has a library reference but no local model link']
|
|
157
|
+
}
|
|
158
|
+
if (status === 'unresolved') {
|
|
159
|
+
return ['component has no local library reference or model link']
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const reasons = []
|
|
163
|
+
const current =
|
|
164
|
+
linkedImplementations.find(
|
|
165
|
+
(implementation) => implementation.isCurrent
|
|
166
|
+
) || linkedImplementations[0]
|
|
167
|
+
if (!current?.modelName) reasons.push('current model name is missing')
|
|
168
|
+
if (!(current?.targetLibraries || []).length) {
|
|
169
|
+
reasons.push('current model target library is missing')
|
|
170
|
+
}
|
|
171
|
+
if (!component.libReference) {
|
|
172
|
+
reasons.push('component library reference is missing')
|
|
173
|
+
}
|
|
174
|
+
return reasons
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Flattens target-library rows without implementation-local indexes.
|
|
179
|
+
* @param {object[]} linkedImplementations Linked implementation rows.
|
|
180
|
+
* @returns {object[]}
|
|
181
|
+
*/
|
|
182
|
+
static #targetLibraries(linkedImplementations) {
|
|
183
|
+
const libraries = []
|
|
184
|
+
const seen = new Set()
|
|
185
|
+
|
|
186
|
+
for (const implementation of linkedImplementations) {
|
|
187
|
+
for (const library of implementation.targetLibraries || []) {
|
|
188
|
+
const publicLibrary = {
|
|
189
|
+
entity: library.entity || '',
|
|
190
|
+
kind: library.kind || '',
|
|
191
|
+
fileName: library.fileName || ''
|
|
192
|
+
}
|
|
193
|
+
const key = JSON.stringify(publicLibrary)
|
|
194
|
+
if (seen.has(key)) continue
|
|
195
|
+
seen.add(key)
|
|
196
|
+
libraries.push(publicLibrary)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return libraries
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Builds status counters.
|
|
205
|
+
* @param {object[]} components Binding rows.
|
|
206
|
+
* @returns {object}
|
|
207
|
+
*/
|
|
208
|
+
static #summary(components) {
|
|
209
|
+
const summary = {
|
|
210
|
+
componentCount: components.length,
|
|
211
|
+
resolvedCount: 0,
|
|
212
|
+
unresolvedCount: 0,
|
|
213
|
+
staleCount: 0,
|
|
214
|
+
externalCount: 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const component of components) {
|
|
218
|
+
summary[component.status + 'Count'] += 1
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return summary
|
|
222
|
+
}
|
|
223
|
+
}
|