altium-toolkit 1.1.24 → 1.1.26
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 +5 -3
- package/docs/api.md +43 -17
- package/docs/model-format.md +51 -11
- package/docs/schemas/altium_toolkit/library_compatibility_a1.schema.json +116 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/spec/library-compatibility.md +65 -0
- package/src/core/altium/AltiumParser.mjs +8 -1
- package/src/core/altium/LibraryCompatibilityGeometry.mjs +618 -0
- package/src/core/altium/LibraryCompatibilityModelHintBuilder.mjs +296 -0
- package/src/core/altium/LibraryCompatibilityReportBuilder.mjs +930 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +29 -1
- package/src/core/altium/PcbDimensionParser.mjs +1 -0
- package/src/core/altium/PcbDimensionReportBuilder.mjs +190 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +140 -10
- package/src/core/altium/PcbFabricationReadinessReportBuilder.mjs +607 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +56 -3
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +138 -3
- package/src/core/altium/PcbLibModelParser.mjs +24 -2
- package/src/core/altium/PcbLibStorageNameResolver.mjs +171 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +15 -93
- package/src/core/altium/PcbPrimitiveFieldOffsetReportBuilder.mjs +352 -0
- package/src/core/altium/PcbRuleImpactReportBuilder.mjs +313 -0
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +514 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +442 -2
- package/src/core/altium/SchematicImageDiagnosticsBuilder.mjs +214 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +523 -3
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +36 -0
- package/src/ui/PcbDimensionPrimitiveRenderer.mjs +422 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +6 -0
- package/src/ui/PcbScene3dBuilder.mjs +10 -2
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +225 -0
- package/src/ui/PcbSvgRenderer.mjs +78 -2
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic fabrication-readiness review items from PCB pads/vias.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbFabricationReadinessReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.pcb.fabrication-readiness.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a fabrication-readiness report from a parser root or PCB model.
|
|
13
|
+
* @param {object} input Parser root, PCB model, or options object.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const pcb = PcbFabricationReadinessReportBuilder.#pcb(input)
|
|
18
|
+
const pads = Array.isArray(pcb?.pads) ? pcb.pads : []
|
|
19
|
+
const vias = Array.isArray(pcb?.vias) ? pcb.vias : []
|
|
20
|
+
const items = [
|
|
21
|
+
...PcbFabricationReadinessReportBuilder.#padItems(pads),
|
|
22
|
+
...PcbFabricationReadinessReportBuilder.#viaItems(vias)
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
schema: PcbFabricationReadinessReportBuilder.SCHEMA,
|
|
27
|
+
summary: PcbFabricationReadinessReportBuilder.#summary(
|
|
28
|
+
pads,
|
|
29
|
+
vias,
|
|
30
|
+
items
|
|
31
|
+
),
|
|
32
|
+
items,
|
|
33
|
+
byCode: PcbFabricationReadinessReportBuilder.#byCode(items)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the PCB payload from a parser root or direct PCB model.
|
|
39
|
+
* @param {object} input Parser root or PCB model.
|
|
40
|
+
* @returns {object}
|
|
41
|
+
*/
|
|
42
|
+
static #pcb(input) {
|
|
43
|
+
return input?.pcb || input || {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds fabrication review items for pads.
|
|
48
|
+
* @param {object[]} pads Normalized pad rows.
|
|
49
|
+
* @returns {object[]}
|
|
50
|
+
*/
|
|
51
|
+
static #padItems(pads) {
|
|
52
|
+
return (pads || []).flatMap((pad, index) => {
|
|
53
|
+
const items = []
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
PcbFabricationReadinessReportBuilder.#hasNonSimplePadMode(pad)
|
|
57
|
+
) {
|
|
58
|
+
items.push(
|
|
59
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
60
|
+
'pcb.fabrication.pad.non-simple-mode',
|
|
61
|
+
'info',
|
|
62
|
+
'pad',
|
|
63
|
+
index,
|
|
64
|
+
pad,
|
|
65
|
+
{
|
|
66
|
+
padMode: pad.padMode,
|
|
67
|
+
padModeName: pad.padModeName
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (PcbFabricationReadinessReportBuilder.#hasPadOffset(pad)) {
|
|
73
|
+
items.push(
|
|
74
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
75
|
+
'pcb.fabrication.pad.offset-center',
|
|
76
|
+
'warning',
|
|
77
|
+
'pad',
|
|
78
|
+
index,
|
|
79
|
+
pad,
|
|
80
|
+
{
|
|
81
|
+
offsets:
|
|
82
|
+
PcbFabricationReadinessReportBuilder.#padOffsets(
|
|
83
|
+
pad
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
if (PcbFabricationReadinessReportBuilder.#hasSlotHole(pad)) {
|
|
90
|
+
items.push(
|
|
91
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
92
|
+
'pcb.fabrication.pad.slotted-hole',
|
|
93
|
+
'info',
|
|
94
|
+
'pad',
|
|
95
|
+
index,
|
|
96
|
+
pad,
|
|
97
|
+
{
|
|
98
|
+
holeGeometry: pad.holeGeometry || null,
|
|
99
|
+
holeSlotLength: pad.holeSlotLength
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (PcbFabricationReadinessReportBuilder.#hasNonPlatedHole(pad)) {
|
|
105
|
+
items.push(
|
|
106
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
107
|
+
'pcb.fabrication.pad.non-plated-hole',
|
|
108
|
+
'warning',
|
|
109
|
+
'pad',
|
|
110
|
+
index,
|
|
111
|
+
pad,
|
|
112
|
+
{}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
PcbFabricationReadinessReportBuilder.#hasMaskOverride(
|
|
118
|
+
pad,
|
|
119
|
+
'paste'
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
items.push(
|
|
123
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
124
|
+
'pcb.fabrication.pad.paste-mask-override',
|
|
125
|
+
'info',
|
|
126
|
+
'pad',
|
|
127
|
+
index,
|
|
128
|
+
pad,
|
|
129
|
+
{
|
|
130
|
+
expansion: pad.pasteMaskExpansion,
|
|
131
|
+
mode: pad.pasteMaskExpansionMode,
|
|
132
|
+
source: pad.pasteMaskExpansionSource
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
PcbFabricationReadinessReportBuilder.#hasMaskOverride(
|
|
139
|
+
pad,
|
|
140
|
+
'solder'
|
|
141
|
+
)
|
|
142
|
+
) {
|
|
143
|
+
items.push(
|
|
144
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
145
|
+
'pcb.fabrication.pad.solder-mask-override',
|
|
146
|
+
'info',
|
|
147
|
+
'pad',
|
|
148
|
+
index,
|
|
149
|
+
pad,
|
|
150
|
+
{
|
|
151
|
+
expansion: pad.solderMaskExpansion,
|
|
152
|
+
mode: pad.solderMaskExpansionMode,
|
|
153
|
+
source: pad.solderMaskExpansionSource
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
if (PcbFabricationReadinessReportBuilder.#hasThermalRelief(pad)) {
|
|
159
|
+
items.push(
|
|
160
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
161
|
+
'pcb.fabrication.pad.thermal-relief',
|
|
162
|
+
'info',
|
|
163
|
+
'pad',
|
|
164
|
+
index,
|
|
165
|
+
pad,
|
|
166
|
+
PcbFabricationReadinessReportBuilder.#thermalDetails(
|
|
167
|
+
pad
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return items
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds fabrication review items for vias.
|
|
179
|
+
* @param {object[]} vias Normalized via rows.
|
|
180
|
+
* @returns {object[]}
|
|
181
|
+
*/
|
|
182
|
+
static #viaItems(vias) {
|
|
183
|
+
return (vias || []).flatMap((via, index) => {
|
|
184
|
+
const items = []
|
|
185
|
+
|
|
186
|
+
if (PcbFabricationReadinessReportBuilder.#hasViaSpan(via)) {
|
|
187
|
+
items.push(
|
|
188
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
189
|
+
'pcb.fabrication.via.layer-span',
|
|
190
|
+
'warning',
|
|
191
|
+
'via',
|
|
192
|
+
index,
|
|
193
|
+
via,
|
|
194
|
+
{
|
|
195
|
+
drillLayerPairType: via.drillLayerPairType,
|
|
196
|
+
diameterByLayer: via.diameterByLayer || [],
|
|
197
|
+
externalStackEntries: via.externalStackEntries || []
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
if (PcbFabricationReadinessReportBuilder.#hasViaProtection(via)) {
|
|
203
|
+
items.push(
|
|
204
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
205
|
+
'pcb.fabrication.via.protected-hole',
|
|
206
|
+
'info',
|
|
207
|
+
'via',
|
|
208
|
+
index,
|
|
209
|
+
via,
|
|
210
|
+
{
|
|
211
|
+
ipc4761Type:
|
|
212
|
+
via.ipc4761Type ??
|
|
213
|
+
via.viaProtection?.ipc4761Type,
|
|
214
|
+
structureType: via.viaProtection?.structureType
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
PcbFabricationReadinessReportBuilder.#hasMaskOverride(
|
|
221
|
+
via,
|
|
222
|
+
'solder'
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
items.push(
|
|
226
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
227
|
+
'pcb.fabrication.via.solder-mask-override',
|
|
228
|
+
'info',
|
|
229
|
+
'via',
|
|
230
|
+
index,
|
|
231
|
+
via,
|
|
232
|
+
{
|
|
233
|
+
expansion: via.solderMaskExpansion,
|
|
234
|
+
mode: via.solderMaskExpansionMode,
|
|
235
|
+
fromHoleEdge: via.solderMaskExpansionFromHoleEdge
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
if (PcbFabricationReadinessReportBuilder.#hasThermalRelief(via)) {
|
|
241
|
+
items.push(
|
|
242
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
243
|
+
'pcb.fabrication.via.thermal-relief',
|
|
244
|
+
'info',
|
|
245
|
+
'via',
|
|
246
|
+
index,
|
|
247
|
+
via,
|
|
248
|
+
PcbFabricationReadinessReportBuilder.#thermalDetails(
|
|
249
|
+
via
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
if (PcbFabricationReadinessReportBuilder.#isMicroviaLike(via)) {
|
|
255
|
+
items.push(
|
|
256
|
+
PcbFabricationReadinessReportBuilder.#item(
|
|
257
|
+
'pcb.fabrication.via.microvia-like',
|
|
258
|
+
'warning',
|
|
259
|
+
'via',
|
|
260
|
+
index,
|
|
261
|
+
via,
|
|
262
|
+
{
|
|
263
|
+
diameter: via.diameter,
|
|
264
|
+
holeDiameter: via.holeDiameter,
|
|
265
|
+
drillLayerPairType: via.drillLayerPairType
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return items
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Builds the deterministic report summary.
|
|
277
|
+
* @param {object[]} pads Normalized pad rows.
|
|
278
|
+
* @param {object[]} vias Normalized via rows.
|
|
279
|
+
* @param {object[]} items Review items.
|
|
280
|
+
* @returns {object}
|
|
281
|
+
*/
|
|
282
|
+
static #summary(pads, vias, items) {
|
|
283
|
+
return {
|
|
284
|
+
padCount: pads.length,
|
|
285
|
+
viaCount: vias.length,
|
|
286
|
+
reviewItemCount: items.length,
|
|
287
|
+
nonSimplePadModeCount:
|
|
288
|
+
PcbFabricationReadinessReportBuilder.#countCode(
|
|
289
|
+
items,
|
|
290
|
+
'pcb.fabrication.pad.non-simple-mode'
|
|
291
|
+
),
|
|
292
|
+
offsetPadCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
293
|
+
items,
|
|
294
|
+
'pcb.fabrication.pad.offset-center'
|
|
295
|
+
),
|
|
296
|
+
slottedHoleCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
297
|
+
items,
|
|
298
|
+
'pcb.fabrication.pad.slotted-hole'
|
|
299
|
+
),
|
|
300
|
+
nonPlatedHoleCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
301
|
+
items,
|
|
302
|
+
'pcb.fabrication.pad.non-plated-hole'
|
|
303
|
+
),
|
|
304
|
+
pasteOverrideCount:
|
|
305
|
+
PcbFabricationReadinessReportBuilder.#countSuffix(
|
|
306
|
+
items,
|
|
307
|
+
'.paste-mask-override'
|
|
308
|
+
),
|
|
309
|
+
maskOverrideCount:
|
|
310
|
+
PcbFabricationReadinessReportBuilder.#countSuffix(
|
|
311
|
+
items,
|
|
312
|
+
'.solder-mask-override'
|
|
313
|
+
),
|
|
314
|
+
thermalReliefCount:
|
|
315
|
+
PcbFabricationReadinessReportBuilder.#countSuffix(
|
|
316
|
+
items,
|
|
317
|
+
'.thermal-relief'
|
|
318
|
+
),
|
|
319
|
+
viaSpanCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
320
|
+
items,
|
|
321
|
+
'pcb.fabrication.via.layer-span'
|
|
322
|
+
),
|
|
323
|
+
protectedViaCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
324
|
+
items,
|
|
325
|
+
'pcb.fabrication.via.protected-hole'
|
|
326
|
+
),
|
|
327
|
+
microviaLikeCount: PcbFabricationReadinessReportBuilder.#countCode(
|
|
328
|
+
items,
|
|
329
|
+
'pcb.fabrication.via.microvia-like'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Builds one normalized review item.
|
|
336
|
+
* @param {string} code Stable issue code.
|
|
337
|
+
* @param {'info' | 'warning'} severity Issue severity.
|
|
338
|
+
* @param {'pad' | 'via'} ownerKind Primitive kind.
|
|
339
|
+
* @param {number} index Primitive index.
|
|
340
|
+
* @param {object} owner Primitive row.
|
|
341
|
+
* @param {object} details Additional issue details.
|
|
342
|
+
* @returns {object}
|
|
343
|
+
*/
|
|
344
|
+
static #item(code, severity, ownerKind, index, owner, details) {
|
|
345
|
+
return PcbFabricationReadinessReportBuilder.#stripEmpty({
|
|
346
|
+
code,
|
|
347
|
+
severity,
|
|
348
|
+
ownerKind,
|
|
349
|
+
ownerKey: ownerKind + '-' + index,
|
|
350
|
+
index,
|
|
351
|
+
designator: owner?.designator,
|
|
352
|
+
netName: owner?.netName,
|
|
353
|
+
layerId: owner?.layerId,
|
|
354
|
+
...details
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Checks whether a pad uses a non-simple local stack mode.
|
|
360
|
+
* @param {object} pad Pad row.
|
|
361
|
+
* @returns {boolean}
|
|
362
|
+
*/
|
|
363
|
+
static #hasNonSimplePadMode(pad) {
|
|
364
|
+
const modeName = String(pad?.padModeName || '').trim()
|
|
365
|
+
if (modeName) {
|
|
366
|
+
return modeName !== 'simple'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const mode = Number(pad?.padMode)
|
|
370
|
+
return Number.isFinite(mode) && mode !== 0
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Checks whether a pad has non-zero per-layer center offsets.
|
|
375
|
+
* @param {object} pad Pad row.
|
|
376
|
+
* @returns {boolean}
|
|
377
|
+
*/
|
|
378
|
+
static #hasPadOffset(pad) {
|
|
379
|
+
return PcbFabricationReadinessReportBuilder.#padOffsets(pad).length > 0
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Collects non-zero pad offsets from local-stack or extension metadata.
|
|
384
|
+
* @param {object} pad Pad row.
|
|
385
|
+
* @returns {object[]}
|
|
386
|
+
*/
|
|
387
|
+
static #padOffsets(pad) {
|
|
388
|
+
const offsets = []
|
|
389
|
+
|
|
390
|
+
for (const layer of pad?.localStack?.layers || []) {
|
|
391
|
+
const offsetX = Number(layer?.offsetX || 0)
|
|
392
|
+
const offsetY = Number(layer?.offsetY || 0)
|
|
393
|
+
if (offsetX || offsetY) {
|
|
394
|
+
offsets.push(
|
|
395
|
+
PcbFabricationReadinessReportBuilder.#stripEmpty({
|
|
396
|
+
layerKey: layer.layerKey,
|
|
397
|
+
role: layer.role,
|
|
398
|
+
offsetX,
|
|
399
|
+
offsetY
|
|
400
|
+
})
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const offset of pad?.layerOffsets || []) {
|
|
405
|
+
const offsetX = Number(offset?.x || offset?.offsetX || 0)
|
|
406
|
+
const offsetY = Number(offset?.y || offset?.offsetY || 0)
|
|
407
|
+
if (offsetX || offsetY) {
|
|
408
|
+
offsets.push(
|
|
409
|
+
PcbFabricationReadinessReportBuilder.#stripEmpty({
|
|
410
|
+
layerNumber: offset.layerNumber,
|
|
411
|
+
layerKey: offset.layerKey,
|
|
412
|
+
offsetX,
|
|
413
|
+
offsetY
|
|
414
|
+
})
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const keyPair of [
|
|
419
|
+
['offsetTopX', 'offsetTopY'],
|
|
420
|
+
['offsetMidX', 'offsetMidY'],
|
|
421
|
+
['offsetBottomX', 'offsetBottomY']
|
|
422
|
+
]) {
|
|
423
|
+
const offsetX = Number(pad?.[keyPair[0]] || 0)
|
|
424
|
+
const offsetY = Number(pad?.[keyPair[1]] || 0)
|
|
425
|
+
if (offsetX || offsetY) {
|
|
426
|
+
offsets.push({
|
|
427
|
+
field: keyPair[0].replace(/X$/u, ''),
|
|
428
|
+
offsetX,
|
|
429
|
+
offsetY
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return offsets
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Checks whether a pad has slot-hole geometry.
|
|
439
|
+
* @param {object} pad Pad row.
|
|
440
|
+
* @returns {boolean}
|
|
441
|
+
*/
|
|
442
|
+
static #hasSlotHole(pad) {
|
|
443
|
+
return (
|
|
444
|
+
pad?.holeGeometry?.shapeName === 'slot' ||
|
|
445
|
+
pad?.holeShapeName === 'slot' ||
|
|
446
|
+
Number(pad?.holeShape) === 2 ||
|
|
447
|
+
Number(pad?.holeSlotLength || 0) > Number(pad?.holeDiameter || 0)
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Checks whether a pad has a non-plated drilled hole.
|
|
453
|
+
* @param {object} pad Pad row.
|
|
454
|
+
* @returns {boolean}
|
|
455
|
+
*/
|
|
456
|
+
static #hasNonPlatedHole(pad) {
|
|
457
|
+
const hasHole =
|
|
458
|
+
Number(pad?.holeDiameter || 0) > 0 ||
|
|
459
|
+
PcbFabricationReadinessReportBuilder.#hasSlotHole(pad)
|
|
460
|
+
|
|
461
|
+
return hasHole && pad?.isPlated === false
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Checks whether a primitive has a manual mask expansion override.
|
|
466
|
+
* @param {object} primitive Primitive row.
|
|
467
|
+
* @param {'paste' | 'solder'} kind Mask kind.
|
|
468
|
+
* @returns {boolean}
|
|
469
|
+
*/
|
|
470
|
+
static #hasMaskOverride(primitive, kind) {
|
|
471
|
+
const prefix = kind === 'paste' ? 'paste' : 'solder'
|
|
472
|
+
const source = String(
|
|
473
|
+
primitive?.[prefix + 'MaskExpansionSource'] || ''
|
|
474
|
+
).trim()
|
|
475
|
+
const mode = Number(primitive?.[prefix + 'MaskExpansionMode'])
|
|
476
|
+
const expansion = Number(primitive?.[prefix + 'MaskExpansion'])
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
source === 'manual' ||
|
|
480
|
+
mode === 2 ||
|
|
481
|
+
(!Number.isNaN(expansion) && expansion !== 0 && !source)
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Checks whether a pad or via has thermal-relief metadata.
|
|
487
|
+
* @param {object} primitive Primitive row.
|
|
488
|
+
* @returns {boolean}
|
|
489
|
+
*/
|
|
490
|
+
static #hasThermalRelief(primitive) {
|
|
491
|
+
return [
|
|
492
|
+
'thermalReliefAirGap',
|
|
493
|
+
'thermalReliefConductorWidth',
|
|
494
|
+
'thermalReliefConductorCount',
|
|
495
|
+
'powerPlaneReliefExpansion'
|
|
496
|
+
].some((key) => Number(primitive?.[key] || 0) !== 0)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Builds compact thermal-relief details.
|
|
501
|
+
* @param {object} primitive Primitive row.
|
|
502
|
+
* @returns {object}
|
|
503
|
+
*/
|
|
504
|
+
static #thermalDetails(primitive) {
|
|
505
|
+
return PcbFabricationReadinessReportBuilder.#stripEmpty({
|
|
506
|
+
planeConnectionStyle: primitive?.planeConnectionStyle,
|
|
507
|
+
thermalReliefAirGap: primitive?.thermalReliefAirGap,
|
|
508
|
+
thermalReliefConductorWidth: primitive?.thermalReliefConductorWidth,
|
|
509
|
+
thermalReliefConductorCount: primitive?.thermalReliefConductorCount,
|
|
510
|
+
powerPlaneReliefExpansion: primitive?.powerPlaneReliefExpansion
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Checks whether a via carries non-default layer-span metadata.
|
|
516
|
+
* @param {object} via Via row.
|
|
517
|
+
* @returns {boolean}
|
|
518
|
+
*/
|
|
519
|
+
static #hasViaSpan(via) {
|
|
520
|
+
return (
|
|
521
|
+
Number(via?.drillLayerPairType || 0) !== 0 ||
|
|
522
|
+
(Array.isArray(via?.diameterByLayer) &&
|
|
523
|
+
via.diameterByLayer.length > 0) ||
|
|
524
|
+
(Array.isArray(via?.externalStackEntries) &&
|
|
525
|
+
via.externalStackEntries.length > 0)
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Checks whether a via has protection or filled/capped metadata.
|
|
531
|
+
* @param {object} via Via row.
|
|
532
|
+
* @returns {boolean}
|
|
533
|
+
*/
|
|
534
|
+
static #hasViaProtection(via) {
|
|
535
|
+
return (
|
|
536
|
+
via?.viaProtection !== undefined ||
|
|
537
|
+
via?.ipc4761Type !== undefined ||
|
|
538
|
+
via?.drill?.renderState === 'covered' ||
|
|
539
|
+
via?.drill?.renderState === 'filled' ||
|
|
540
|
+
via?.drill?.renderState === 'capped'
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Checks whether via geometry resembles a small layer-span microvia.
|
|
546
|
+
* @param {object} via Via row.
|
|
547
|
+
* @returns {boolean}
|
|
548
|
+
*/
|
|
549
|
+
static #isMicroviaLike(via) {
|
|
550
|
+
const holeDiameter = Number(via?.holeDiameter || 0)
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
holeDiameter > 0 &&
|
|
554
|
+
holeDiameter <= 6 &&
|
|
555
|
+
PcbFabricationReadinessReportBuilder.#hasViaSpan(via)
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Counts items with one exact issue code.
|
|
561
|
+
* @param {object[]} items Review items.
|
|
562
|
+
* @param {string} code Issue code.
|
|
563
|
+
* @returns {number}
|
|
564
|
+
*/
|
|
565
|
+
static #countCode(items, code) {
|
|
566
|
+
return items.filter((item) => item.code === code).length
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Counts items whose code ends with a suffix.
|
|
571
|
+
* @param {object[]} items Review items.
|
|
572
|
+
* @param {string} suffix Issue-code suffix.
|
|
573
|
+
* @returns {number}
|
|
574
|
+
*/
|
|
575
|
+
static #countSuffix(items, suffix) {
|
|
576
|
+
return items.filter((item) => item.code.endsWith(suffix)).length
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Builds code counters for report consumers.
|
|
581
|
+
* @param {object[]} items Review items.
|
|
582
|
+
* @returns {object[]}
|
|
583
|
+
*/
|
|
584
|
+
static #byCode(items) {
|
|
585
|
+
const counts = new Map()
|
|
586
|
+
|
|
587
|
+
for (const item of items) {
|
|
588
|
+
counts.set(item.code, Number(counts.get(item.code) || 0) + 1)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return [...counts.entries()].map(([code, count]) => ({ code, count }))
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Removes empty fields while preserving zeros and false.
|
|
596
|
+
* @param {Record<string, unknown>} row Input row.
|
|
597
|
+
* @returns {Record<string, unknown>}
|
|
598
|
+
*/
|
|
599
|
+
static #stripEmpty(row) {
|
|
600
|
+
return Object.fromEntries(
|
|
601
|
+
Object.entries(row || {}).filter(([, value]) => {
|
|
602
|
+
if (Array.isArray(value)) return value.length > 0
|
|
603
|
+
return value !== undefined && value !== null && value !== ''
|
|
604
|
+
})
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbClassReportBuilder } from './PcbClassReportBuilder.mjs'
|
|
6
|
+
import { PcbDimensionReportBuilder } from './PcbDimensionReportBuilder.mjs'
|
|
7
|
+
import { PcbFabricationReadinessReportBuilder } from './PcbFabricationReadinessReportBuilder.mjs'
|
|
6
8
|
import { PcbNetMembershipReportBuilder } from './PcbNetMembershipReportBuilder.mjs'
|
|
9
|
+
import { PcbReviewMetadataBuilder } from './PcbReviewMetadataBuilder.mjs'
|
|
7
10
|
import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
|
|
11
|
+
import { PcbRuleImpactReportBuilder } from './PcbRuleImpactReportBuilder.mjs'
|
|
8
12
|
import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -37,6 +41,25 @@ export class PcbInspectionReportBuilder {
|
|
|
37
41
|
input.classReport ||
|
|
38
42
|
pcb.classReport ||
|
|
39
43
|
PcbClassReportBuilder.build(pcb)
|
|
44
|
+
const dimensions =
|
|
45
|
+
input.dimensionReport ||
|
|
46
|
+
pcb.dimensionReport ||
|
|
47
|
+
PcbDimensionReportBuilder.build(pcb)
|
|
48
|
+
const ruleImpact =
|
|
49
|
+
input.ruleImpact ||
|
|
50
|
+
pcb.ruleImpact ||
|
|
51
|
+
PcbRuleImpactReportBuilder.build(pcb)
|
|
52
|
+
const reviewMetadata =
|
|
53
|
+
input.reviewMetadata ||
|
|
54
|
+
pcb.reviewMetadata ||
|
|
55
|
+
PcbReviewMetadataBuilder.build({
|
|
56
|
+
...pcb,
|
|
57
|
+
routeAnalysis
|
|
58
|
+
})
|
|
59
|
+
const fabricationReadiness =
|
|
60
|
+
input.fabricationReadiness ||
|
|
61
|
+
pcb.fabricationReadiness ||
|
|
62
|
+
PcbFabricationReadinessReportBuilder.build(pcb)
|
|
40
63
|
const rules = PcbInspectionReportBuilder.#rules(pcb?.rules || [])
|
|
41
64
|
const primitives = PcbInspectionReportBuilder.#primitives(pcb)
|
|
42
65
|
const diagnosticSummary =
|
|
@@ -49,7 +72,11 @@ export class PcbInspectionReportBuilder {
|
|
|
49
72
|
rules,
|
|
50
73
|
diagnosticSummary,
|
|
51
74
|
netMembership,
|
|
52
|
-
classes
|
|
75
|
+
classes,
|
|
76
|
+
dimensions,
|
|
77
|
+
ruleImpact,
|
|
78
|
+
reviewMetadata,
|
|
79
|
+
fabricationReadiness
|
|
53
80
|
})
|
|
54
81
|
|
|
55
82
|
return {
|
|
@@ -72,6 +99,22 @@ export class PcbInspectionReportBuilder {
|
|
|
72
99
|
},
|
|
73
100
|
netMembership,
|
|
74
101
|
classes,
|
|
102
|
+
dimensions: {
|
|
103
|
+
schema: dimensions.schema,
|
|
104
|
+
summary: dimensions.summary || {}
|
|
105
|
+
},
|
|
106
|
+
ruleImpact: {
|
|
107
|
+
schema: ruleImpact.schema,
|
|
108
|
+
summary: ruleImpact.summary || {}
|
|
109
|
+
},
|
|
110
|
+
reviewMetadata: {
|
|
111
|
+
schema: reviewMetadata.schema,
|
|
112
|
+
summary: reviewMetadata.summary || {}
|
|
113
|
+
},
|
|
114
|
+
fabricationReadiness: {
|
|
115
|
+
schema: fabricationReadiness.schema,
|
|
116
|
+
summary: fabricationReadiness.summary || {}
|
|
117
|
+
},
|
|
75
118
|
routeAnalysis: {
|
|
76
119
|
schema: routeAnalysis.schema,
|
|
77
120
|
summary: routeAnalysis.summary || {}
|
|
@@ -261,7 +304,9 @@ export class PcbInspectionReportBuilder {
|
|
|
261
304
|
parts.diagnosticSummary.warningCount +
|
|
262
305
|
Number(parts.netMembership.summary.undeclaredNetCount || 0) +
|
|
263
306
|
Number(parts.netMembership.summary.unownedPrimitiveCount || 0) +
|
|
264
|
-
Number(parts.classes.summary.unresolvedMemberCount || 0)
|
|
307
|
+
Number(parts.classes.summary.unresolvedMemberCount || 0) +
|
|
308
|
+
Number(parts.dimensions.summary.unresolvedCount || 0) +
|
|
309
|
+
Number(parts.fabricationReadiness.summary.reviewItemCount || 0)
|
|
265
310
|
|
|
266
311
|
return {
|
|
267
312
|
fileName: parts.input.fileName || '',
|
|
@@ -282,7 +327,15 @@ export class PcbInspectionReportBuilder {
|
|
|
282
327
|
warningCount: parts.diagnosticSummary.warningCount,
|
|
283
328
|
possibleUnroutedNetCount:
|
|
284
329
|
parts.netMembership.summary.possibleUnroutedNetCount || 0,
|
|
285
|
-
classIssueCount: parts.classes.summary.issueCount || 0
|
|
330
|
+
classIssueCount: parts.classes.summary.issueCount || 0,
|
|
331
|
+
dimensionCount: parts.dimensions.summary.dimensionCount || 0,
|
|
332
|
+
dimensionIssueCount: parts.dimensions.summary.unresolvedCount || 0,
|
|
333
|
+
manufacturingRuleCount:
|
|
334
|
+
parts.ruleImpact.summary.manufacturingRuleCount || 0,
|
|
335
|
+
polygonRealizationCount:
|
|
336
|
+
parts.reviewMetadata.summary.polygonRealizationCount || 0,
|
|
337
|
+
fabricationReviewItemCount:
|
|
338
|
+
parts.fabricationReadiness.summary.reviewItemCount || 0
|
|
286
339
|
}
|
|
287
340
|
}
|
|
288
341
|
|