altium-toolkit 1.1.22 → 1.1.23
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 +34 -5
- package/docs/api.md +134 -23
- package/docs/model-format.md +174 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +511 -1
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +2 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumParser.mjs +352 -14
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +9 -0
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +12 -45
- package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/parser.mjs +35 -1
- package/src/renderers.mjs +1 -0
- package/src/ui/SchematicShapeRenderer.mjs +49 -6
- package/src/ui/SchematicSvgRenderer.mjs +37 -8
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { LibraryQaReportBuilder } from './LibraryQaReportBuilder.mjs'
|
|
6
|
+
import { LibraryRenderManifestBuilder } from './LibraryRenderManifestBuilder.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds deterministic static catalog artifacts for parsed libraries.
|
|
10
|
+
*/
|
|
11
|
+
export class LibraryCatalogArtifactBuilder {
|
|
12
|
+
static SCHEMA_ID = 'altium-toolkit.library.catalog.a1'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds catalog entries, search metadata, and static HTML.
|
|
16
|
+
* @param {{ schematicLibraries?: object[], pcbLibraries?: object[], qaReport?: object }} [options] Catalog options.
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
static build(options = {}) {
|
|
20
|
+
const schematicLibraries = options.schematicLibraries || []
|
|
21
|
+
const pcbLibraries = options.pcbLibraries || []
|
|
22
|
+
const qaReport =
|
|
23
|
+
options.qaReport ||
|
|
24
|
+
LibraryQaReportBuilder.build({ schematicLibraries, pcbLibraries })
|
|
25
|
+
const issuesByTarget =
|
|
26
|
+
LibraryCatalogArtifactBuilder.#issuesByTarget(qaReport)
|
|
27
|
+
const entries = [
|
|
28
|
+
...LibraryCatalogArtifactBuilder.#symbolEntries(
|
|
29
|
+
schematicLibraries,
|
|
30
|
+
issuesByTarget
|
|
31
|
+
),
|
|
32
|
+
...LibraryCatalogArtifactBuilder.#footprintEntries(
|
|
33
|
+
pcbLibraries,
|
|
34
|
+
issuesByTarget
|
|
35
|
+
)
|
|
36
|
+
].sort(
|
|
37
|
+
(left, right) =>
|
|
38
|
+
left.kind.localeCompare(right.kind) ||
|
|
39
|
+
left.name.localeCompare(right.name) ||
|
|
40
|
+
left.libraryFileName.localeCompare(right.libraryFileName)
|
|
41
|
+
)
|
|
42
|
+
const searchIndex = {
|
|
43
|
+
schema: 'altium-toolkit.library.catalog-search.a1',
|
|
44
|
+
entries: entries.map((entry) => ({
|
|
45
|
+
key: entry.key,
|
|
46
|
+
kind: entry.kind,
|
|
47
|
+
name: entry.name,
|
|
48
|
+
libraryFileName: entry.libraryFileName,
|
|
49
|
+
text: entry.searchText
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
schema: LibraryCatalogArtifactBuilder.SCHEMA_ID,
|
|
55
|
+
summary: {
|
|
56
|
+
schematicLibraryCount: schematicLibraries.length,
|
|
57
|
+
pcbLibraryCount: pcbLibraries.length,
|
|
58
|
+
entryCount: entries.length,
|
|
59
|
+
symbolCount: entries.filter((entry) => entry.kind === 'symbol')
|
|
60
|
+
.length,
|
|
61
|
+
footprintCount: entries.filter(
|
|
62
|
+
(entry) => entry.kind === 'footprint'
|
|
63
|
+
).length,
|
|
64
|
+
issueCount: entries.reduce(
|
|
65
|
+
(count, entry) => count + entry.issueCodes.length,
|
|
66
|
+
0
|
|
67
|
+
)
|
|
68
|
+
},
|
|
69
|
+
entries,
|
|
70
|
+
searchIndex,
|
|
71
|
+
html: LibraryCatalogArtifactBuilder.#html(entries)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds catalog entries for schematic symbols.
|
|
77
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
78
|
+
* @param {Map<string, object[]>} issuesByTarget Issues by target.
|
|
79
|
+
* @returns {object[]}
|
|
80
|
+
*/
|
|
81
|
+
static #symbolEntries(schematicLibraries, issuesByTarget) {
|
|
82
|
+
return (schematicLibraries || []).flatMap((library) => {
|
|
83
|
+
const schematicLibrary = library.schematicLibrary || library || {}
|
|
84
|
+
const manifest =
|
|
85
|
+
LibraryRenderManifestBuilder.buildSchematicLibraryManifest(
|
|
86
|
+
schematicLibrary
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return (schematicLibrary.symbols || []).map(
|
|
90
|
+
(symbol, symbolIndex) => {
|
|
91
|
+
const name = String(symbol.name || '').trim()
|
|
92
|
+
const output = manifest.outputs.find(
|
|
93
|
+
(entry) => entry.name === name
|
|
94
|
+
)
|
|
95
|
+
const issues =
|
|
96
|
+
issuesByTarget.get('symbol:' + name) ||
|
|
97
|
+
issuesByTarget.get(name) ||
|
|
98
|
+
[]
|
|
99
|
+
|
|
100
|
+
return LibraryCatalogArtifactBuilder.#stripUndefined({
|
|
101
|
+
key: 'symbol:' + (library.fileName || '') + ':' + name,
|
|
102
|
+
kind: 'symbol',
|
|
103
|
+
name,
|
|
104
|
+
libraryFileName: library.fileName || '',
|
|
105
|
+
index: symbolIndex,
|
|
106
|
+
pinCount: (symbol.pins || []).length,
|
|
107
|
+
partCount: (symbol.parts || []).length,
|
|
108
|
+
outputSvgKey: output?.outputSvgKey,
|
|
109
|
+
issueCodes: issues.map((issue) => issue.code),
|
|
110
|
+
issues,
|
|
111
|
+
searchText: [
|
|
112
|
+
name,
|
|
113
|
+
library.fileName || '',
|
|
114
|
+
'symbol',
|
|
115
|
+
symbol.parameters?.Description || '',
|
|
116
|
+
'pins:' + (symbol.pins || []).length
|
|
117
|
+
]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join(' ')
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds catalog entries for PCB footprints.
|
|
128
|
+
* @param {object[]} pcbLibraries PCB footprint libraries.
|
|
129
|
+
* @param {Map<string, object[]>} issuesByTarget Issues by target.
|
|
130
|
+
* @returns {object[]}
|
|
131
|
+
*/
|
|
132
|
+
static #footprintEntries(pcbLibraries, issuesByTarget) {
|
|
133
|
+
return (pcbLibraries || []).flatMap((library) => {
|
|
134
|
+
const pcbLibrary = library.pcbLibrary || library || {}
|
|
135
|
+
const manifest =
|
|
136
|
+
LibraryRenderManifestBuilder.buildPcbLibraryManifest(pcbLibrary)
|
|
137
|
+
|
|
138
|
+
return (pcbLibrary.footprints || []).map(
|
|
139
|
+
(footprint, footprintIndex) => {
|
|
140
|
+
const name = String(footprint.name || '').trim()
|
|
141
|
+
const output = manifest.outputs.find(
|
|
142
|
+
(entry) => entry.name === name
|
|
143
|
+
)
|
|
144
|
+
const issues =
|
|
145
|
+
issuesByTarget.get('footprint:' + name) ||
|
|
146
|
+
issuesByTarget.get(name) ||
|
|
147
|
+
[]
|
|
148
|
+
|
|
149
|
+
return LibraryCatalogArtifactBuilder.#stripUndefined({
|
|
150
|
+
key:
|
|
151
|
+
'footprint:' +
|
|
152
|
+
(library.fileName || '') +
|
|
153
|
+
':' +
|
|
154
|
+
name,
|
|
155
|
+
kind: 'footprint',
|
|
156
|
+
name,
|
|
157
|
+
libraryFileName: library.fileName || '',
|
|
158
|
+
index: footprintIndex,
|
|
159
|
+
primitiveCount:
|
|
160
|
+
(footprint.pads || []).length +
|
|
161
|
+
(footprint.tracks || []).length +
|
|
162
|
+
(footprint.arcs || []).length +
|
|
163
|
+
(footprint.vias || []).length +
|
|
164
|
+
(footprint.fills || []).length +
|
|
165
|
+
(footprint.regions || []).length,
|
|
166
|
+
rawRecordCount: (footprint.rawRecords || []).length,
|
|
167
|
+
outputSvgKey: output?.outputSvgKey,
|
|
168
|
+
issueCodes: issues.map((issue) => issue.code),
|
|
169
|
+
issues,
|
|
170
|
+
searchText: [
|
|
171
|
+
name,
|
|
172
|
+
library.fileName || '',
|
|
173
|
+
'footprint',
|
|
174
|
+
'pads:' + (footprint.pads || []).length
|
|
175
|
+
]
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.join(' ')
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Builds a target-key lookup for QA issues.
|
|
186
|
+
* @param {object} qaReport QA report.
|
|
187
|
+
* @returns {Map<string, object[]>}
|
|
188
|
+
*/
|
|
189
|
+
static #issuesByTarget(qaReport) {
|
|
190
|
+
const byTarget = new Map()
|
|
191
|
+
const issues = [
|
|
192
|
+
...(qaReport?.libraryLint?.issues || []),
|
|
193
|
+
...(qaReport?.issues || [])
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
for (const issue of issues) {
|
|
197
|
+
const keys = new Set([
|
|
198
|
+
issue.target,
|
|
199
|
+
issue.symbolName ? 'symbol:' + issue.symbolName : '',
|
|
200
|
+
issue.footprintName ? 'footprint:' + issue.footprintName : ''
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
if (!key) continue
|
|
205
|
+
byTarget.set(key, [...(byTarget.get(key) || []), issue])
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return byTarget
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds a self-contained static HTML catalog.
|
|
214
|
+
* @param {object[]} entries Catalog entries.
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
static #html(entries) {
|
|
218
|
+
const items = entries
|
|
219
|
+
.map((entry) => {
|
|
220
|
+
const badges = entry.issueCodes
|
|
221
|
+
.map(
|
|
222
|
+
(code) =>
|
|
223
|
+
'<span class="badge">' +
|
|
224
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(code) +
|
|
225
|
+
'</span>'
|
|
226
|
+
)
|
|
227
|
+
.join('')
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
'<article data-catalog-entry="' +
|
|
231
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(entry.kind) +
|
|
232
|
+
'">' +
|
|
233
|
+
'<h2>' +
|
|
234
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(entry.name) +
|
|
235
|
+
'</h2>' +
|
|
236
|
+
'<p>' +
|
|
237
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(
|
|
238
|
+
entry.libraryFileName
|
|
239
|
+
) +
|
|
240
|
+
'</p>' +
|
|
241
|
+
'<p data-preview-key="' +
|
|
242
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(
|
|
243
|
+
entry.outputSvgKey || ''
|
|
244
|
+
) +
|
|
245
|
+
'">' +
|
|
246
|
+
LibraryCatalogArtifactBuilder.#escapeHtml(
|
|
247
|
+
entry.outputSvgKey || ''
|
|
248
|
+
) +
|
|
249
|
+
'</p>' +
|
|
250
|
+
'<div class="badges">' +
|
|
251
|
+
badges +
|
|
252
|
+
'</div>' +
|
|
253
|
+
'</article>'
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
.join('')
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
'<!doctype html><html><head><meta charset="utf-8">' +
|
|
260
|
+
'<title>Library Catalog</title>' +
|
|
261
|
+
'<style>body{font-family:sans-serif;margin:24px}' +
|
|
262
|
+
'main{display:grid;gap:12px}' +
|
|
263
|
+
'article{border:1px solid #d0d7de;border-radius:6px;padding:12px}' +
|
|
264
|
+
'.badge{display:inline-block;margin:4px 4px 0 0;padding:2px 6px;' +
|
|
265
|
+
'border:1px solid #d0d7de;border-radius:999px;font-size:12px}' +
|
|
266
|
+
'</style></head><body><main data-library-catalog>' +
|
|
267
|
+
items +
|
|
268
|
+
'</main></body></html>'
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Escapes text for HTML output.
|
|
274
|
+
* @param {unknown} value Source value.
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
static #escapeHtml(value) {
|
|
278
|
+
return String(value ?? '')
|
|
279
|
+
.replace(/&/gu, '&')
|
|
280
|
+
.replace(/</gu, '<')
|
|
281
|
+
.replace(/>/gu, '>')
|
|
282
|
+
.replace(/"/gu, '"')
|
|
283
|
+
.replace(/'/gu, ''')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Removes undefined fields.
|
|
288
|
+
* @param {object} row Source row.
|
|
289
|
+
* @returns {object}
|
|
290
|
+
*/
|
|
291
|
+
static #stripUndefined(row) {
|
|
292
|
+
return Object.fromEntries(
|
|
293
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic read-only diffs between parsed library models.
|
|
7
|
+
*/
|
|
8
|
+
export class LibraryDiffReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.library.diff.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a library diff report.
|
|
13
|
+
* @param {{ left?: object[] | object, right?: object[] | object }} input Diff input.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const leftLibraries = LibraryDiffReportBuilder.#libraries(input.left)
|
|
18
|
+
const rightLibraries = LibraryDiffReportBuilder.#libraries(input.right)
|
|
19
|
+
const symbols = LibraryDiffReportBuilder.#diffCollection(
|
|
20
|
+
LibraryDiffReportBuilder.#symbolRows(leftLibraries),
|
|
21
|
+
LibraryDiffReportBuilder.#symbolRows(rightLibraries),
|
|
22
|
+
'symbol'
|
|
23
|
+
)
|
|
24
|
+
const footprints = LibraryDiffReportBuilder.#diffCollection(
|
|
25
|
+
LibraryDiffReportBuilder.#footprintRows(leftLibraries),
|
|
26
|
+
LibraryDiffReportBuilder.#footprintRows(rightLibraries),
|
|
27
|
+
'footprint'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
schema: LibraryDiffReportBuilder.SCHEMA,
|
|
32
|
+
summary: {
|
|
33
|
+
leftLibraryCount: leftLibraries.length,
|
|
34
|
+
rightLibraryCount: rightLibraries.length,
|
|
35
|
+
addedSymbolCount: symbols.added.length,
|
|
36
|
+
removedSymbolCount: symbols.removed.length,
|
|
37
|
+
changedSymbolCount: symbols.changed.length,
|
|
38
|
+
addedFootprintCount: footprints.added.length,
|
|
39
|
+
removedFootprintCount: footprints.removed.length,
|
|
40
|
+
changedFootprintCount: footprints.changed.length,
|
|
41
|
+
differenceCount:
|
|
42
|
+
symbols.added.length +
|
|
43
|
+
symbols.removed.length +
|
|
44
|
+
symbols.changed.length +
|
|
45
|
+
footprints.added.length +
|
|
46
|
+
footprints.removed.length +
|
|
47
|
+
footprints.changed.length
|
|
48
|
+
},
|
|
49
|
+
symbols,
|
|
50
|
+
footprints
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalizes library input to an array.
|
|
56
|
+
* @param {object[] | object | undefined} value Input value.
|
|
57
|
+
* @returns {object[]}
|
|
58
|
+
*/
|
|
59
|
+
static #libraries(value) {
|
|
60
|
+
if (!value) return []
|
|
61
|
+
return Array.isArray(value) ? value : [value]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collects schematic symbol comparison rows.
|
|
66
|
+
* @param {object[]} libraries Library models.
|
|
67
|
+
* @returns {object[]}
|
|
68
|
+
*/
|
|
69
|
+
static #symbolRows(libraries) {
|
|
70
|
+
return (libraries || []).flatMap((library) =>
|
|
71
|
+
(library.schematicLibrary?.symbols || []).map((symbol, index) => ({
|
|
72
|
+
name: String(symbol.name || ''),
|
|
73
|
+
libraryFileName: library.fileName || '',
|
|
74
|
+
index,
|
|
75
|
+
pinCount: (symbol.pins || []).length,
|
|
76
|
+
partCount: (symbol.parts || []).length,
|
|
77
|
+
parameters: symbol.parameters || {}
|
|
78
|
+
}))
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Collects PCB footprint comparison rows.
|
|
84
|
+
* @param {object[]} libraries Library models.
|
|
85
|
+
* @returns {object[]}
|
|
86
|
+
*/
|
|
87
|
+
static #footprintRows(libraries) {
|
|
88
|
+
return (libraries || []).flatMap((library) =>
|
|
89
|
+
(library.pcbLibrary?.footprints || []).map((footprint, index) => ({
|
|
90
|
+
name: String(footprint.name || ''),
|
|
91
|
+
libraryFileName: library.fileName || '',
|
|
92
|
+
index,
|
|
93
|
+
padCount: (footprint.pads || []).length,
|
|
94
|
+
textCount: (footprint.texts || []).length,
|
|
95
|
+
parameters: footprint.parameters || {}
|
|
96
|
+
}))
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Diffs one named library item collection.
|
|
102
|
+
* @param {object[]} leftRows Left rows.
|
|
103
|
+
* @param {object[]} rightRows Right rows.
|
|
104
|
+
* @param {'symbol' | 'footprint'} kind Collection kind.
|
|
105
|
+
* @returns {{ added: object[], removed: object[], changed: object[] }}
|
|
106
|
+
*/
|
|
107
|
+
static #diffCollection(leftRows, rightRows, kind) {
|
|
108
|
+
const leftByName = LibraryDiffReportBuilder.#byName(leftRows)
|
|
109
|
+
const rightByName = LibraryDiffReportBuilder.#byName(rightRows)
|
|
110
|
+
const names = [
|
|
111
|
+
...new Set([...leftByName.keys(), ...rightByName.keys()])
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.sort((left, right) => left.localeCompare(right))
|
|
115
|
+
const added = []
|
|
116
|
+
const removed = []
|
|
117
|
+
const changed = []
|
|
118
|
+
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const left = leftByName.get(name)
|
|
121
|
+
const right = rightByName.get(name)
|
|
122
|
+
|
|
123
|
+
if (!left && right) {
|
|
124
|
+
added.push(
|
|
125
|
+
LibraryDiffReportBuilder.#publicRow(right, kind, {
|
|
126
|
+
includeName: true
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (left && !right) {
|
|
133
|
+
removed.push(
|
|
134
|
+
LibraryDiffReportBuilder.#publicRow(left, kind, {
|
|
135
|
+
includeName: true
|
|
136
|
+
})
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const differences = LibraryDiffReportBuilder.#differences(
|
|
142
|
+
left,
|
|
143
|
+
right,
|
|
144
|
+
kind
|
|
145
|
+
)
|
|
146
|
+
if (Object.keys(differences).length) {
|
|
147
|
+
changed.push({
|
|
148
|
+
name,
|
|
149
|
+
left: LibraryDiffReportBuilder.#publicRow(left, kind),
|
|
150
|
+
right: LibraryDiffReportBuilder.#publicRow(right, kind),
|
|
151
|
+
differences
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { added, removed, changed }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Indexes rows by name.
|
|
161
|
+
* @param {object[]} rows Rows.
|
|
162
|
+
* @returns {Map<string, object>}
|
|
163
|
+
*/
|
|
164
|
+
static #byName(rows) {
|
|
165
|
+
return new Map(
|
|
166
|
+
(rows || []).filter((row) => row.name).map((row) => [row.name, row])
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Builds public row shape without private comparison-only data.
|
|
172
|
+
* @param {object} row Source row.
|
|
173
|
+
* @param {'symbol' | 'footprint'} kind Collection kind.
|
|
174
|
+
* @param {{ includeName?: boolean }} options Row options.
|
|
175
|
+
* @returns {object}
|
|
176
|
+
*/
|
|
177
|
+
static #publicRow(row, kind, options = {}) {
|
|
178
|
+
const base = {
|
|
179
|
+
libraryFileName: row.libraryFileName,
|
|
180
|
+
index: row.index
|
|
181
|
+
}
|
|
182
|
+
if (options.includeName) {
|
|
183
|
+
base.name = row.name
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (kind === 'symbol') {
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
pinCount: row.pinCount,
|
|
190
|
+
partCount: row.partCount
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
padCount: row.padCount,
|
|
197
|
+
textCount: row.textCount
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds differences between two matching rows.
|
|
203
|
+
* @param {object} left Left row.
|
|
204
|
+
* @param {object} right Right row.
|
|
205
|
+
* @param {'symbol' | 'footprint'} kind Collection kind.
|
|
206
|
+
* @returns {object}
|
|
207
|
+
*/
|
|
208
|
+
static #differences(left, right, kind) {
|
|
209
|
+
const differences = {}
|
|
210
|
+
|
|
211
|
+
for (const key of kind === 'symbol'
|
|
212
|
+
? ['pinCount', 'partCount']
|
|
213
|
+
: ['padCount', 'textCount']) {
|
|
214
|
+
if (left[key] !== right[key]) {
|
|
215
|
+
differences[key] = { left: left[key], right: right[key] }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const parameterDifferences =
|
|
220
|
+
LibraryDiffReportBuilder.#parameterDifferences(
|
|
221
|
+
left.parameters,
|
|
222
|
+
right.parameters
|
|
223
|
+
)
|
|
224
|
+
if (Object.keys(parameterDifferences).length) {
|
|
225
|
+
differences.parameters = parameterDifferences
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return differences
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Builds per-parameter value differences.
|
|
233
|
+
* @param {Record<string, string>} left Left parameters.
|
|
234
|
+
* @param {Record<string, string>} right Right parameters.
|
|
235
|
+
* @returns {object}
|
|
236
|
+
*/
|
|
237
|
+
static #parameterDifferences(left, right) {
|
|
238
|
+
const differences = {}
|
|
239
|
+
const keys = [
|
|
240
|
+
...new Set([
|
|
241
|
+
...Object.keys(left || {}),
|
|
242
|
+
...Object.keys(right || {})
|
|
243
|
+
])
|
|
244
|
+
].sort((leftKey, rightKey) => leftKey.localeCompare(rightKey))
|
|
245
|
+
|
|
246
|
+
for (const key of keys) {
|
|
247
|
+
const leftValue = String(left?.[key] ?? '')
|
|
248
|
+
const rightValue = String(right?.[key] ?? '')
|
|
249
|
+
|
|
250
|
+
if (leftValue !== rightValue) {
|
|
251
|
+
differences[key] = {
|
|
252
|
+
left: leftValue,
|
|
253
|
+
right: rightValue
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return differences
|
|
259
|
+
}
|
|
260
|
+
}
|