altium-toolkit 1.0.9 → 1.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.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic QA reports across parsed schematic and PCB libraries.
|
|
7
|
+
*/
|
|
8
|
+
export class LibraryQaReportBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.library.qa.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a read-only library QA report.
|
|
13
|
+
* @param {{ schematicLibraries?: object[], pcbLibraries?: object[] }} options Library collections.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(options = {}) {
|
|
17
|
+
const schematicLibraries = options.schematicLibraries || []
|
|
18
|
+
const pcbLibraries = options.pcbLibraries || []
|
|
19
|
+
const duplicateSymbols =
|
|
20
|
+
LibraryQaReportBuilder.#duplicateSymbols(schematicLibraries)
|
|
21
|
+
const duplicateFootprints =
|
|
22
|
+
LibraryQaReportBuilder.#duplicateFootprints(pcbLibraries)
|
|
23
|
+
const staleImplementations =
|
|
24
|
+
LibraryQaReportBuilder.#staleImplementations(
|
|
25
|
+
schematicLibraries,
|
|
26
|
+
pcbLibraries
|
|
27
|
+
)
|
|
28
|
+
const missingModels =
|
|
29
|
+
LibraryQaReportBuilder.#missingModels(pcbLibraries)
|
|
30
|
+
const multipartMismatches =
|
|
31
|
+
LibraryQaReportBuilder.#multipartMismatches(schematicLibraries)
|
|
32
|
+
const mergePlan =
|
|
33
|
+
LibraryQaReportBuilder.#schematicLibraryMergePlan(
|
|
34
|
+
schematicLibraries
|
|
35
|
+
)
|
|
36
|
+
const issues = [
|
|
37
|
+
...duplicateSymbols.map((issue) =>
|
|
38
|
+
LibraryQaReportBuilder.#issue(
|
|
39
|
+
'library.duplicate-symbol',
|
|
40
|
+
issue.name
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
...duplicateFootprints.map((issue) =>
|
|
44
|
+
LibraryQaReportBuilder.#issue(
|
|
45
|
+
'library.duplicate-footprint',
|
|
46
|
+
issue.name
|
|
47
|
+
)
|
|
48
|
+
),
|
|
49
|
+
...staleImplementations.map((issue) =>
|
|
50
|
+
LibraryQaReportBuilder.#issue(
|
|
51
|
+
'library.stale-implementation',
|
|
52
|
+
issue.symbolName
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
...missingModels.map((issue) =>
|
|
56
|
+
LibraryQaReportBuilder.#issue(
|
|
57
|
+
'library.missing-model',
|
|
58
|
+
issue.footprintName
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
...multipartMismatches.map((issue) =>
|
|
62
|
+
LibraryQaReportBuilder.#issue(
|
|
63
|
+
'library.multipart-mismatch',
|
|
64
|
+
issue.symbolName
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
...mergePlan.diagnostics.map((diagnostic) =>
|
|
68
|
+
LibraryQaReportBuilder.#issue(
|
|
69
|
+
diagnostic.code,
|
|
70
|
+
diagnostic.symbolName
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
schema: LibraryQaReportBuilder.SCHEMA_ID,
|
|
77
|
+
summary: {
|
|
78
|
+
schematicLibraryCount: schematicLibraries.length,
|
|
79
|
+
pcbLibraryCount: pcbLibraries.length,
|
|
80
|
+
duplicateSymbolCount: duplicateSymbols.length,
|
|
81
|
+
duplicateFootprintCount: duplicateFootprints.length,
|
|
82
|
+
staleImplementationCount: staleImplementations.length,
|
|
83
|
+
missingModelCount: missingModels.length,
|
|
84
|
+
multipartMismatchCount: multipartMismatches.length,
|
|
85
|
+
mergePlanConflictCount: mergePlan.summary.conflictCount,
|
|
86
|
+
issueCount: issues.length
|
|
87
|
+
},
|
|
88
|
+
duplicates: {
|
|
89
|
+
symbols: duplicateSymbols,
|
|
90
|
+
footprints: duplicateFootprints
|
|
91
|
+
},
|
|
92
|
+
staleImplementations,
|
|
93
|
+
missingModels,
|
|
94
|
+
multipartMismatches,
|
|
95
|
+
mergePlan,
|
|
96
|
+
issues
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Finds duplicate schematic symbols by name.
|
|
102
|
+
* @param {object[]} libraries Schematic library models.
|
|
103
|
+
* @returns {object[]}
|
|
104
|
+
*/
|
|
105
|
+
static #duplicateSymbols(libraries) {
|
|
106
|
+
const byName = new Map()
|
|
107
|
+
|
|
108
|
+
for (const library of libraries || []) {
|
|
109
|
+
const fileName = library.fileName || ''
|
|
110
|
+
for (const [index, symbol] of (
|
|
111
|
+
library.schematicLibrary?.symbols || []
|
|
112
|
+
).entries()) {
|
|
113
|
+
const name = String(symbol.name || '').trim()
|
|
114
|
+
if (!name) continue
|
|
115
|
+
byName.set(name, [
|
|
116
|
+
...(byName.get(name) || []),
|
|
117
|
+
{ libraryFileName: fileName, index }
|
|
118
|
+
])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [...byName.entries()]
|
|
123
|
+
.filter(([, occurrences]) => occurrences.length > 1)
|
|
124
|
+
.map(([name, occurrences]) => ({ name, occurrences }))
|
|
125
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Finds duplicate PCB footprints and classifies shape collisions.
|
|
130
|
+
* @param {object[]} libraries PCB library models.
|
|
131
|
+
* @returns {object[]}
|
|
132
|
+
*/
|
|
133
|
+
static #duplicateFootprints(libraries) {
|
|
134
|
+
const byName = new Map()
|
|
135
|
+
|
|
136
|
+
for (const library of libraries || []) {
|
|
137
|
+
const fileName = library.fileName || ''
|
|
138
|
+
for (const [index, footprint] of (
|
|
139
|
+
library.pcbLibrary?.footprints || []
|
|
140
|
+
).entries()) {
|
|
141
|
+
const name = String(footprint.name || '').trim()
|
|
142
|
+
if (!name) continue
|
|
143
|
+
byName.set(name, [
|
|
144
|
+
...(byName.get(name) || []),
|
|
145
|
+
{
|
|
146
|
+
libraryFileName: fileName,
|
|
147
|
+
index,
|
|
148
|
+
padCount: (footprint.pads || []).length
|
|
149
|
+
}
|
|
150
|
+
])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [...byName.entries()]
|
|
155
|
+
.filter(([, occurrences]) => occurrences.length > 1)
|
|
156
|
+
.map(([name, occurrences]) => ({
|
|
157
|
+
name,
|
|
158
|
+
occurrences,
|
|
159
|
+
collisionKind:
|
|
160
|
+
LibraryQaReportBuilder.#footprintCollisionKind(occurrences)
|
|
161
|
+
}))
|
|
162
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Finds implementation rows that target absent PCB library files.
|
|
167
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
168
|
+
* @param {object[]} pcbLibraries PCB libraries.
|
|
169
|
+
* @returns {object[]}
|
|
170
|
+
*/
|
|
171
|
+
static #staleImplementations(schematicLibraries, pcbLibraries) {
|
|
172
|
+
const availablePcbLibraries = new Set(
|
|
173
|
+
(pcbLibraries || []).map((library) => library.fileName || '')
|
|
174
|
+
)
|
|
175
|
+
const issues = []
|
|
176
|
+
|
|
177
|
+
for (const library of schematicLibraries || []) {
|
|
178
|
+
for (const symbol of library.schematicLibrary?.symbols || []) {
|
|
179
|
+
for (const implementation of symbol.implementations || []) {
|
|
180
|
+
const targetLibraries = implementation.targetLibraries || []
|
|
181
|
+
const hasMissingTarget = targetLibraries.some(
|
|
182
|
+
(target) => !availablePcbLibraries.has(target)
|
|
183
|
+
)
|
|
184
|
+
if (!hasMissingTarget) continue
|
|
185
|
+
issues.push({
|
|
186
|
+
libraryFileName: library.fileName || '',
|
|
187
|
+
symbolName: symbol.name || '',
|
|
188
|
+
modelName: implementation.modelName || '',
|
|
189
|
+
targetLibraries,
|
|
190
|
+
reason: 'target library was not present in the scanned collection'
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return issues
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Finds footprint component bodies that reference missing embedded models.
|
|
201
|
+
* @param {object[]} pcbLibraries PCB libraries.
|
|
202
|
+
* @returns {object[]}
|
|
203
|
+
*/
|
|
204
|
+
static #missingModels(pcbLibraries) {
|
|
205
|
+
const issues = []
|
|
206
|
+
|
|
207
|
+
for (const library of pcbLibraries || []) {
|
|
208
|
+
for (const footprint of library.pcbLibrary?.footprints || []) {
|
|
209
|
+
const modelIds = new Set(
|
|
210
|
+
(footprint.embeddedModels || []).map((model) =>
|
|
211
|
+
String(model.id || model.modelId || '')
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
for (const body of footprint.componentBodies || []) {
|
|
215
|
+
const modelId = String(body.modelId || body.id || '')
|
|
216
|
+
if (!modelId || modelIds.has(modelId)) continue
|
|
217
|
+
issues.push({
|
|
218
|
+
libraryFileName: library.fileName || '',
|
|
219
|
+
footprintName: footprint.name || '',
|
|
220
|
+
modelId,
|
|
221
|
+
reason: 'component body references an embedded model that is absent'
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return issues
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Finds multipart symbols whose part ids skip expected alphabetical parts.
|
|
232
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
233
|
+
* @returns {object[]}
|
|
234
|
+
*/
|
|
235
|
+
static #multipartMismatches(schematicLibraries) {
|
|
236
|
+
const issues = []
|
|
237
|
+
|
|
238
|
+
for (const library of schematicLibraries || []) {
|
|
239
|
+
for (const symbol of library.schematicLibrary?.symbols || []) {
|
|
240
|
+
const partIds = (symbol.parts || [])
|
|
241
|
+
.map((part) => String(part.partId || '').trim())
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
if (partIds.length < 2) continue
|
|
244
|
+
const expectedPartIds = LibraryQaReportBuilder.#expectedPartIds(
|
|
245
|
+
partIds.length
|
|
246
|
+
)
|
|
247
|
+
if (partIds.join('\u0000') === expectedPartIds.join('\u0000')) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
issues.push({
|
|
251
|
+
libraryFileName: library.fileName || '',
|
|
252
|
+
symbolName: symbol.name || '',
|
|
253
|
+
partIds,
|
|
254
|
+
expectedPartIds
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return issues
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Builds a read-only merge plan for schematic libraries.
|
|
264
|
+
* @param {object[]} schematicLibraries Schematic library models.
|
|
265
|
+
* @returns {object}
|
|
266
|
+
*/
|
|
267
|
+
static #schematicLibraryMergePlan(schematicLibraries) {
|
|
268
|
+
const duplicateSymbols =
|
|
269
|
+
LibraryQaReportBuilder.#mergePlanDuplicateSymbols(
|
|
270
|
+
schematicLibraries
|
|
271
|
+
)
|
|
272
|
+
const embeddedAssets =
|
|
273
|
+
LibraryQaReportBuilder.#mergePlanEmbeddedAssets(schematicLibraries)
|
|
274
|
+
const fontDependencies =
|
|
275
|
+
LibraryQaReportBuilder.#mergePlanFontDependencies(
|
|
276
|
+
schematicLibraries
|
|
277
|
+
)
|
|
278
|
+
const diagnostics = duplicateSymbols
|
|
279
|
+
.filter(
|
|
280
|
+
(duplicate) => duplicate.conflictKind === 'conflicting-symbol'
|
|
281
|
+
)
|
|
282
|
+
.map((duplicate) => ({
|
|
283
|
+
code: 'library.merge-plan.conflicting-symbol',
|
|
284
|
+
severity: 'warning',
|
|
285
|
+
symbolName: duplicate.name
|
|
286
|
+
}))
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
schema: 'altium-toolkit.library.merge-plan.a1',
|
|
290
|
+
strategy: 'read-only-analysis',
|
|
291
|
+
summary: {
|
|
292
|
+
duplicateNameCount: duplicateSymbols.length,
|
|
293
|
+
conflictCount: diagnostics.length,
|
|
294
|
+
renameSuggestionCount: duplicateSymbols.reduce(
|
|
295
|
+
(count, duplicate) =>
|
|
296
|
+
count +
|
|
297
|
+
Math.max(duplicate.suggestedNames.length - 1, 0),
|
|
298
|
+
0
|
|
299
|
+
),
|
|
300
|
+
embeddedAssetCount: embeddedAssets.length,
|
|
301
|
+
fontDependencyCount: fontDependencies.length
|
|
302
|
+
},
|
|
303
|
+
duplicateSymbols,
|
|
304
|
+
embeddedAssets,
|
|
305
|
+
fontDependencies,
|
|
306
|
+
diagnostics
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Builds duplicate-symbol merge-plan rows.
|
|
312
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
313
|
+
* @returns {object[]}
|
|
314
|
+
*/
|
|
315
|
+
static #mergePlanDuplicateSymbols(schematicLibraries) {
|
|
316
|
+
const byName = new Map()
|
|
317
|
+
|
|
318
|
+
for (const library of schematicLibraries || []) {
|
|
319
|
+
const fileName = library.fileName || ''
|
|
320
|
+
for (const [index, symbol] of (
|
|
321
|
+
library.schematicLibrary?.symbols || []
|
|
322
|
+
).entries()) {
|
|
323
|
+
const name = String(symbol.name || '').trim()
|
|
324
|
+
if (!name) continue
|
|
325
|
+
byName.set(name, [
|
|
326
|
+
...(byName.get(name) || []),
|
|
327
|
+
LibraryQaReportBuilder.#mergePlanSymbolOccurrence(
|
|
328
|
+
fileName,
|
|
329
|
+
index,
|
|
330
|
+
symbol
|
|
331
|
+
)
|
|
332
|
+
])
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return [...byName.entries()]
|
|
337
|
+
.filter(([, occurrences]) => occurrences.length > 1)
|
|
338
|
+
.map(([name, occurrences]) => {
|
|
339
|
+
const differences =
|
|
340
|
+
LibraryQaReportBuilder.#mergePlanDifferences(occurrences)
|
|
341
|
+
return {
|
|
342
|
+
name,
|
|
343
|
+
conflictKind: Object.keys(differences).length
|
|
344
|
+
? 'conflicting-symbol'
|
|
345
|
+
: 'duplicate-name',
|
|
346
|
+
suggestedNames: occurrences.map((occurrence, index) => ({
|
|
347
|
+
libraryFileName: occurrence.libraryFileName,
|
|
348
|
+
index: occurrence.index,
|
|
349
|
+
currentName: name,
|
|
350
|
+
suggestedName:
|
|
351
|
+
index === 0 ? name : name + '_' + (index + 1)
|
|
352
|
+
})),
|
|
353
|
+
...(Object.keys(differences).length ? { differences } : {}),
|
|
354
|
+
occurrences
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Builds one duplicate-symbol occurrence summary.
|
|
362
|
+
* @param {string} libraryFileName Library file name.
|
|
363
|
+
* @param {number} index Symbol index.
|
|
364
|
+
* @param {object} symbol Symbol row.
|
|
365
|
+
* @returns {object}
|
|
366
|
+
*/
|
|
367
|
+
static #mergePlanSymbolOccurrence(libraryFileName, index, symbol) {
|
|
368
|
+
return {
|
|
369
|
+
libraryFileName,
|
|
370
|
+
index,
|
|
371
|
+
pinCount: (symbol.pins || []).length,
|
|
372
|
+
partCount: (symbol.parts || []).length,
|
|
373
|
+
displayModeCount: (
|
|
374
|
+
symbol.displayModes ||
|
|
375
|
+
symbol.displayModeCatalog ||
|
|
376
|
+
[]
|
|
377
|
+
).length
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Builds differing-count metadata for duplicate symbols.
|
|
383
|
+
* @param {object[]} occurrences Duplicate occurrences.
|
|
384
|
+
* @returns {object}
|
|
385
|
+
*/
|
|
386
|
+
static #mergePlanDifferences(occurrences) {
|
|
387
|
+
return LibraryQaReportBuilder.#stripEmpty({
|
|
388
|
+
pinCounts: LibraryQaReportBuilder.#differingCounts(
|
|
389
|
+
occurrences,
|
|
390
|
+
'pinCount'
|
|
391
|
+
),
|
|
392
|
+
partCounts: LibraryQaReportBuilder.#differingCounts(
|
|
393
|
+
occurrences,
|
|
394
|
+
'partCount'
|
|
395
|
+
),
|
|
396
|
+
displayModeCounts: LibraryQaReportBuilder.#differingCounts(
|
|
397
|
+
occurrences,
|
|
398
|
+
'displayModeCount'
|
|
399
|
+
)
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Returns differing values for one occurrence count key.
|
|
405
|
+
* @param {object[]} occurrences Occurrence rows.
|
|
406
|
+
* @param {string} key Count key.
|
|
407
|
+
* @returns {number[] | undefined}
|
|
408
|
+
*/
|
|
409
|
+
static #differingCounts(occurrences, key) {
|
|
410
|
+
const values = (occurrences || []).map((occurrence) => occurrence[key])
|
|
411
|
+
return new Set(values).size > 1 ? values : undefined
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Lists embedded assets referenced by schematic symbols.
|
|
416
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
417
|
+
* @returns {object[]}
|
|
418
|
+
*/
|
|
419
|
+
static #mergePlanEmbeddedAssets(schematicLibraries) {
|
|
420
|
+
return (schematicLibraries || []).flatMap((library) =>
|
|
421
|
+
(library.schematicLibrary?.symbols || []).flatMap((symbol) =>
|
|
422
|
+
(symbol.embeddedAssets || symbol.images || []).map((asset) =>
|
|
423
|
+
LibraryQaReportBuilder.#stripEmpty({
|
|
424
|
+
libraryFileName: library.fileName || '',
|
|
425
|
+
symbolName: symbol.name || '',
|
|
426
|
+
...asset
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Lists schematic-library font dependencies.
|
|
435
|
+
* @param {object[]} schematicLibraries Schematic libraries.
|
|
436
|
+
* @returns {object[]}
|
|
437
|
+
*/
|
|
438
|
+
static #mergePlanFontDependencies(schematicLibraries) {
|
|
439
|
+
return (schematicLibraries || []).flatMap((library) =>
|
|
440
|
+
(
|
|
441
|
+
library.schematicLibrary?.fonts ||
|
|
442
|
+
library.schematicLibrary?.embeddedFonts ||
|
|
443
|
+
[]
|
|
444
|
+
).map((font) =>
|
|
445
|
+
LibraryQaReportBuilder.#stripEmpty({
|
|
446
|
+
libraryFileName: library.fileName || '',
|
|
447
|
+
...font
|
|
448
|
+
})
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Classifies whether duplicate footprints appear equivalent.
|
|
455
|
+
* @param {{ padCount: number }[]} occurrences Footprint occurrences.
|
|
456
|
+
* @returns {string}
|
|
457
|
+
*/
|
|
458
|
+
static #footprintCollisionKind(occurrences) {
|
|
459
|
+
const padCounts = new Set(
|
|
460
|
+
(occurrences || []).map((occurrence) => occurrence.padCount)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
return padCounts.size > 1 ? 'conflicting-footprint' : 'duplicate-name'
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Builds expected alphabetical part ids.
|
|
468
|
+
* @param {number} count Part count.
|
|
469
|
+
* @returns {string[]}
|
|
470
|
+
*/
|
|
471
|
+
static #expectedPartIds(count) {
|
|
472
|
+
return Array.from({ length: count }, (_value, index) =>
|
|
473
|
+
String.fromCharCode(65 + index)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Builds a compact issue entry for summary consumers.
|
|
479
|
+
* @param {string} code Diagnostic code.
|
|
480
|
+
* @param {string} target Target object name.
|
|
481
|
+
* @returns {object}
|
|
482
|
+
*/
|
|
483
|
+
static #issue(code, target) {
|
|
484
|
+
return {
|
|
485
|
+
code,
|
|
486
|
+
severity: 'warning',
|
|
487
|
+
target
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Removes undefined and empty-string fields.
|
|
493
|
+
* @param {Record<string, unknown>} value Source object.
|
|
494
|
+
* @returns {object}
|
|
495
|
+
*/
|
|
496
|
+
static #stripEmpty(value) {
|
|
497
|
+
return Object.fromEntries(
|
|
498
|
+
Object.entries(value || {}).filter(
|
|
499
|
+
([, entryValue]) =>
|
|
500
|
+
entryValue !== undefined && entryValue !== ''
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -86,6 +86,32 @@ export class LibraryRenderManifestBuilder {
|
|
|
86
86
|
return {
|
|
87
87
|
schema: 'altium-toolkit.schematic.extraction-manifest.a1',
|
|
88
88
|
sourceDocument: String(documentModel?.fileName || ''),
|
|
89
|
+
summary: {
|
|
90
|
+
outputCount: outputs.length,
|
|
91
|
+
embeddedAssetCount:
|
|
92
|
+
LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
|
|
93
|
+
outputs.flatMap((output) => output.embeddedAssets || [])
|
|
94
|
+
).length,
|
|
95
|
+
readyOutputCount: outputs.filter(
|
|
96
|
+
(output) =>
|
|
97
|
+
output.databaseLibrary?.readiness === 'ready' ||
|
|
98
|
+
!output.databaseLibrary
|
|
99
|
+
).length,
|
|
100
|
+
strippedParameterCount: outputs.reduce(
|
|
101
|
+
(count, output) =>
|
|
102
|
+
count +
|
|
103
|
+
(output.databaseLibrary?.strippedParameterNames
|
|
104
|
+
?.length || 0),
|
|
105
|
+
0
|
|
106
|
+
),
|
|
107
|
+
strippedImplementationCount: outputs.reduce(
|
|
108
|
+
(count, output) =>
|
|
109
|
+
count +
|
|
110
|
+
(output.databaseLibrary?.strippedImplementationKeys
|
|
111
|
+
?.length || 0),
|
|
112
|
+
0
|
|
113
|
+
)
|
|
114
|
+
},
|
|
89
115
|
outputs,
|
|
90
116
|
embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
|
|
91
117
|
outputs.flatMap((output) => output.embeddedAssets || [])
|
|
@@ -93,6 +119,62 @@ export class LibraryRenderManifestBuilder {
|
|
|
93
119
|
}
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Builds a read-only manifest for extracting a schematic template.
|
|
124
|
+
* @param {{ fileName?: string, schematic?: { template?: object } } | { template?: object }} documentModel Parsed schematic document model.
|
|
125
|
+
* @returns {object}
|
|
126
|
+
*/
|
|
127
|
+
static buildSchematicTemplateExtractionManifest(documentModel) {
|
|
128
|
+
const template =
|
|
129
|
+
documentModel?.schematic?.template ||
|
|
130
|
+
documentModel?.template ||
|
|
131
|
+
null
|
|
132
|
+
const identity = template?.identity || {}
|
|
133
|
+
const outputKey =
|
|
134
|
+
'schematic-template/' +
|
|
135
|
+
LibraryRenderManifestBuilder.#slug(
|
|
136
|
+
LibraryRenderManifestBuilder.#withoutExtension(
|
|
137
|
+
identity.fileName || identity.name || 'template'
|
|
138
|
+
)
|
|
139
|
+
) +
|
|
140
|
+
'.schdot'
|
|
141
|
+
const diagnostics = (template?.missingParameters || []).map(
|
|
142
|
+
(parameterName) => ({
|
|
143
|
+
code: 'schematic.template-extraction.missing-parameter',
|
|
144
|
+
severity: 'warning',
|
|
145
|
+
parameterName
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
schema: 'altium-toolkit.schematic.template-extraction.a1',
|
|
151
|
+
sourceDocument: String(documentModel?.fileName || ''),
|
|
152
|
+
template: template
|
|
153
|
+
? {
|
|
154
|
+
identity,
|
|
155
|
+
outputTemplateKey: outputKey,
|
|
156
|
+
renderManifestKey: outputKey.replace(
|
|
157
|
+
/\.schdot$/u,
|
|
158
|
+
'.render.json'
|
|
159
|
+
),
|
|
160
|
+
ownedRecordKeys: template.ownedRecordKeys || [],
|
|
161
|
+
ownedGraphics: template.ownedGraphics || {},
|
|
162
|
+
fonts: template.fonts || {},
|
|
163
|
+
missingParameters: template.missingParameters || [],
|
|
164
|
+
titleBlock: template.titleBlock || {}
|
|
165
|
+
}
|
|
166
|
+
: null,
|
|
167
|
+
summary: {
|
|
168
|
+
templatePresent: Boolean(template),
|
|
169
|
+
ownedRecordCount: (template?.ownedRecordKeys || []).length,
|
|
170
|
+
missingParameterCount: (template?.missingParameters || [])
|
|
171
|
+
.length,
|
|
172
|
+
fontCount: Object.keys(template?.fonts || {}).length
|
|
173
|
+
},
|
|
174
|
+
diagnostics
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
96
178
|
/**
|
|
97
179
|
* Builds render outputs for one schematic symbol.
|
|
98
180
|
* @param {object} symbol Symbol record.
|
|
@@ -202,8 +284,14 @@ export class LibraryRenderManifestBuilder {
|
|
|
202
284
|
)
|
|
203
285
|
)
|
|
204
286
|
)
|
|
287
|
+
const databaseLibrary =
|
|
288
|
+
LibraryRenderManifestBuilder.#databaseLibraryExtractionPlan(
|
|
289
|
+
symbolKey,
|
|
290
|
+
component,
|
|
291
|
+
schematic
|
|
292
|
+
)
|
|
205
293
|
|
|
206
|
-
return {
|
|
294
|
+
return LibraryRenderManifestBuilder.#stripUndefined({
|
|
207
295
|
kind: 'symbol-extraction',
|
|
208
296
|
symbolKey,
|
|
209
297
|
sourceComponent: LibraryRenderManifestBuilder.#stripUndefined({
|
|
@@ -222,8 +310,81 @@ export class LibraryRenderManifestBuilder {
|
|
|
222
310
|
texts: children.texts.length,
|
|
223
311
|
images: children.images.length
|
|
224
312
|
},
|
|
225
|
-
embeddedAssets
|
|
313
|
+
embeddedAssets,
|
|
314
|
+
databaseLibrary
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Builds a database-library audit plan for one extracted symbol.
|
|
320
|
+
* @param {string} symbolKey Symbol extraction key.
|
|
321
|
+
* @param {object} component Source component row.
|
|
322
|
+
* @param {object} schematic Schematic model.
|
|
323
|
+
* @returns {object | undefined}
|
|
324
|
+
*/
|
|
325
|
+
static #databaseLibraryExtractionPlan(symbolKey, component, schematic) {
|
|
326
|
+
const parameters = component?.parameters || {}
|
|
327
|
+
const parameterNames = Object.keys(parameters)
|
|
328
|
+
const strippedParameterNames = parameterNames.filter((name) =>
|
|
329
|
+
LibraryRenderManifestBuilder.#isPlacementParameterName(name)
|
|
330
|
+
)
|
|
331
|
+
const preservedParameterNames = parameterNames.filter(
|
|
332
|
+
(name) =>
|
|
333
|
+
!LibraryRenderManifestBuilder.#isPlacementParameterName(name)
|
|
334
|
+
)
|
|
335
|
+
const strippedImplementationKeys =
|
|
336
|
+
LibraryRenderManifestBuilder.#componentImplementationKeys(
|
|
337
|
+
schematic,
|
|
338
|
+
component
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
strippedParameterNames.length === 0 &&
|
|
343
|
+
preservedParameterNames.length === 0 &&
|
|
344
|
+
strippedImplementationKeys.length === 0
|
|
345
|
+
) {
|
|
346
|
+
return undefined
|
|
226
347
|
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
readiness: 'ready',
|
|
351
|
+
preservedParameterNames,
|
|
352
|
+
strippedParameterNames,
|
|
353
|
+
stripImplementationLinks: strippedImplementationKeys.length > 0,
|
|
354
|
+
strippedImplementationKeys,
|
|
355
|
+
auditKey: 'schematic-extract/' + symbolKey + '.dblib.json'
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Returns true for component-placement parameter names not suitable for
|
|
361
|
+
* extracted library symbols.
|
|
362
|
+
* @param {string} name Parameter name.
|
|
363
|
+
* @returns {boolean}
|
|
364
|
+
*/
|
|
365
|
+
static #isPlacementParameterName(name) {
|
|
366
|
+
return ['designator', 'comment'].includes(
|
|
367
|
+
String(name || '').toLowerCase()
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Finds implementation keys associated with one placed component.
|
|
373
|
+
* @param {object} schematic Schematic model.
|
|
374
|
+
* @param {object} component Component row.
|
|
375
|
+
* @returns {string[]}
|
|
376
|
+
*/
|
|
377
|
+
static #componentImplementationKeys(schematic, component) {
|
|
378
|
+
const ownerIndex = String(component?.ownerIndex || '').trim()
|
|
379
|
+
const componentKey = ownerIndex
|
|
380
|
+
? 'schematic-component-' + ownerIndex
|
|
381
|
+
: ''
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
schematic?.implementations?.components?.find(
|
|
385
|
+
(entry) => entry.componentKey === componentKey
|
|
386
|
+
)?.implementationKeys || []
|
|
387
|
+
)
|
|
227
388
|
}
|
|
228
389
|
|
|
229
390
|
/**
|
|
@@ -414,4 +575,13 @@ export class LibraryRenderManifestBuilder {
|
|
|
414
575
|
.replace(/^-+|-+$/gu, '') || 'item'
|
|
415
576
|
)
|
|
416
577
|
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Removes a final filename extension from a display name.
|
|
581
|
+
* @param {unknown} value Source value.
|
|
582
|
+
* @returns {string}
|
|
583
|
+
*/
|
|
584
|
+
static #withoutExtension(value) {
|
|
585
|
+
return String(value || '').replace(/\.[A-Za-z0-9]+$/u, '')
|
|
586
|
+
}
|
|
417
587
|
}
|