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
|
@@ -10,7 +10,7 @@ export class SchematicConnectivityQaBuilder {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Builds connectivity QA from the normalized single-sheet net model.
|
|
13
|
-
* @param {{ nets?: object[], texts?: object[], pins?: object[], ports?: object[], junctions?: object[], lines?: object[] }} schematic Normalized schematic fragments.
|
|
13
|
+
* @param {{ nets?: object[], texts?: object[], pins?: object[], ports?: object[], junctions?: object[], lines?: object[], sheetEntries?: object[], harnesses?: object | null }} schematic Normalized schematic fragments.
|
|
14
14
|
* @returns {object}
|
|
15
15
|
*/
|
|
16
16
|
static build(schematic) {
|
|
@@ -24,6 +24,10 @@ export class SchematicConnectivityQaBuilder {
|
|
|
24
24
|
const junctions = Array.isArray(schematic?.junctions)
|
|
25
25
|
? schematic.junctions
|
|
26
26
|
: []
|
|
27
|
+
const sheetEntries = Array.isArray(schematic?.sheetEntries)
|
|
28
|
+
? schematic.sheetEntries
|
|
29
|
+
: []
|
|
30
|
+
const harnesses = schematic?.harnesses || null
|
|
27
31
|
const findings = [
|
|
28
32
|
...SchematicConnectivityQaBuilder.#implicitNetFindings(nets),
|
|
29
33
|
...SchematicConnectivityQaBuilder.#danglingLabelFindings(
|
|
@@ -42,7 +46,12 @@ export class SchematicConnectivityQaBuilder {
|
|
|
42
46
|
...SchematicConnectivityQaBuilder.#unjunctionedTeeContactFindings(
|
|
43
47
|
lines,
|
|
44
48
|
junctions
|
|
45
|
-
)
|
|
49
|
+
),
|
|
50
|
+
...SchematicConnectivityQaBuilder.#harnessFindings(
|
|
51
|
+
sheetEntries,
|
|
52
|
+
harnesses
|
|
53
|
+
),
|
|
54
|
+
...SchematicConnectivityQaBuilder.#pinInterpretationFindings(pins)
|
|
46
55
|
]
|
|
47
56
|
|
|
48
57
|
return {
|
|
@@ -214,14 +223,403 @@ export class SchematicConnectivityQaBuilder {
|
|
|
214
223
|
'schematic.connectivity.ambiguous-junction'
|
|
215
224
|
)
|
|
216
225
|
}
|
|
226
|
+
const harnessFindingCount = SchematicConnectivityQaBuilder.#countCode(
|
|
227
|
+
findings,
|
|
228
|
+
'schematic.connectivity.harness-sheet-entry-unresolved-type'
|
|
229
|
+
)
|
|
230
|
+
const unlinkedHarnessEntryCount =
|
|
231
|
+
SchematicConnectivityQaBuilder.#countCode(
|
|
232
|
+
findings,
|
|
233
|
+
'schematic.connectivity.harness-entry-unlinked-signal'
|
|
234
|
+
)
|
|
235
|
+
const harnessTypeMismatchCount =
|
|
236
|
+
SchematicConnectivityQaBuilder.#countCode(
|
|
237
|
+
findings,
|
|
238
|
+
'schematic.connectivity.harness-type-mismatch'
|
|
239
|
+
)
|
|
217
240
|
|
|
218
241
|
if (unjunctionedTeeContactCount) {
|
|
219
242
|
summary.unjunctionedTeeContactCount = unjunctionedTeeContactCount
|
|
220
243
|
}
|
|
244
|
+
if (
|
|
245
|
+
harnessFindingCount ||
|
|
246
|
+
unlinkedHarnessEntryCount ||
|
|
247
|
+
harnessTypeMismatchCount
|
|
248
|
+
) {
|
|
249
|
+
summary.harnessFindingCount =
|
|
250
|
+
harnessFindingCount +
|
|
251
|
+
unlinkedHarnessEntryCount +
|
|
252
|
+
harnessTypeMismatchCount
|
|
253
|
+
summary.unresolvedHarnessTypeCount = harnessFindingCount
|
|
254
|
+
summary.unlinkedHarnessEntryCount = unlinkedHarnessEntryCount
|
|
255
|
+
}
|
|
256
|
+
if (harnessTypeMismatchCount) {
|
|
257
|
+
summary.harnessTypeMismatchCount = harnessTypeMismatchCount
|
|
258
|
+
}
|
|
259
|
+
SchematicConnectivityQaBuilder.#assignPinInterpretationCounts(
|
|
260
|
+
summary,
|
|
261
|
+
findings
|
|
262
|
+
)
|
|
221
263
|
|
|
222
264
|
return summary
|
|
223
265
|
}
|
|
224
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Adds pin interpretation counters to the summary when present.
|
|
269
|
+
* @param {Record<string, number>} summary Mutable summary row.
|
|
270
|
+
* @param {object[]} findings QA findings.
|
|
271
|
+
* @returns {void}
|
|
272
|
+
*/
|
|
273
|
+
static #assignPinInterpretationCounts(summary, findings) {
|
|
274
|
+
const hiddenPinLabelCount = [
|
|
275
|
+
'schematic.pin.hidden-labels',
|
|
276
|
+
'schematic.pin.hidden-name',
|
|
277
|
+
'schematic.pin.hidden-number'
|
|
278
|
+
].reduce(
|
|
279
|
+
(count, code) =>
|
|
280
|
+
count +
|
|
281
|
+
SchematicConnectivityQaBuilder.#countCode(findings, code),
|
|
282
|
+
0
|
|
283
|
+
)
|
|
284
|
+
const powerElectricalAmbiguityCount =
|
|
285
|
+
SchematicConnectivityQaBuilder.#countCode(
|
|
286
|
+
findings,
|
|
287
|
+
'schematic.pin.power-like-name-non-power-electrical'
|
|
288
|
+
)
|
|
289
|
+
const pinEndpointSymbolCount =
|
|
290
|
+
SchematicConnectivityQaBuilder.#countCode(
|
|
291
|
+
findings,
|
|
292
|
+
'schematic.pin.endpoint-symbol'
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if (hiddenPinLabelCount) {
|
|
296
|
+
summary.hiddenPinLabelCount = hiddenPinLabelCount
|
|
297
|
+
}
|
|
298
|
+
if (powerElectricalAmbiguityCount) {
|
|
299
|
+
summary.powerElectricalAmbiguityCount =
|
|
300
|
+
powerElectricalAmbiguityCount
|
|
301
|
+
}
|
|
302
|
+
if (pinEndpointSymbolCount) {
|
|
303
|
+
summary.pinEndpointSymbolCount = pinEndpointSymbolCount
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Builds QA findings for recovered pin display/electrical metadata.
|
|
309
|
+
* @param {object[]} pins Normalized pins.
|
|
310
|
+
* @returns {object[]}
|
|
311
|
+
*/
|
|
312
|
+
static #pinInterpretationFindings(pins) {
|
|
313
|
+
const findings = []
|
|
314
|
+
|
|
315
|
+
for (const pin of pins || []) {
|
|
316
|
+
findings.push(
|
|
317
|
+
...SchematicConnectivityQaBuilder.#pinLabelFindings(pin)
|
|
318
|
+
)
|
|
319
|
+
const powerFinding =
|
|
320
|
+
SchematicConnectivityQaBuilder.#powerElectricalFinding(pin)
|
|
321
|
+
if (powerFinding) {
|
|
322
|
+
findings.push(powerFinding)
|
|
323
|
+
}
|
|
324
|
+
const endpointFinding =
|
|
325
|
+
SchematicConnectivityQaBuilder.#endpointSymbolFinding(pin)
|
|
326
|
+
if (endpointFinding) {
|
|
327
|
+
findings.push(endpointFinding)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return findings
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Builds findings for pin name/number display suppression.
|
|
336
|
+
* @param {object} pin Normalized pin.
|
|
337
|
+
* @returns {object[]}
|
|
338
|
+
*/
|
|
339
|
+
static #pinLabelFindings(pin) {
|
|
340
|
+
const labelMode = String(pin?.labelMode || 'name-and-number')
|
|
341
|
+
const hasName = String(pin?.name || '').trim().length > 0
|
|
342
|
+
const hasNumber = String(pin?.designator || '').trim().length > 0
|
|
343
|
+
|
|
344
|
+
if (labelMode === 'hidden' && (hasName || hasNumber)) {
|
|
345
|
+
return [
|
|
346
|
+
SchematicConnectivityQaBuilder.#pinFinding(
|
|
347
|
+
'schematic.pin.hidden-labels',
|
|
348
|
+
'info',
|
|
349
|
+
pin
|
|
350
|
+
)
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
if (labelMode === 'number-only' && hasName) {
|
|
354
|
+
return [
|
|
355
|
+
SchematicConnectivityQaBuilder.#pinFinding(
|
|
356
|
+
'schematic.pin.hidden-name',
|
|
357
|
+
'info',
|
|
358
|
+
pin
|
|
359
|
+
)
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
if (labelMode === 'name-only' && hasNumber) {
|
|
363
|
+
return [
|
|
364
|
+
SchematicConnectivityQaBuilder.#pinFinding(
|
|
365
|
+
'schematic.pin.hidden-number',
|
|
366
|
+
'info',
|
|
367
|
+
pin
|
|
368
|
+
)
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return []
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Builds a finding when a power-like pin name does not use a power type.
|
|
377
|
+
* @param {object} pin Normalized pin.
|
|
378
|
+
* @returns {object | null}
|
|
379
|
+
*/
|
|
380
|
+
static #powerElectricalFinding(pin) {
|
|
381
|
+
if (
|
|
382
|
+
!SchematicConnectivityQaBuilder.#isPowerLikePinName(pin?.name) ||
|
|
383
|
+
SchematicConnectivityQaBuilder.#isPowerElectricalType(
|
|
384
|
+
pin?.electrical
|
|
385
|
+
)
|
|
386
|
+
) {
|
|
387
|
+
return null
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return SchematicConnectivityQaBuilder.#pinFinding(
|
|
391
|
+
'schematic.pin.power-like-name-non-power-electrical',
|
|
392
|
+
'warning',
|
|
393
|
+
pin,
|
|
394
|
+
{
|
|
395
|
+
electrical: pin?.electrical
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Builds a finding when a pin carries an endpoint symbol marker.
|
|
402
|
+
* @param {object} pin Normalized pin.
|
|
403
|
+
* @returns {object | null}
|
|
404
|
+
*/
|
|
405
|
+
static #endpointSymbolFinding(pin) {
|
|
406
|
+
const symbolOuter = Number(pin?.symbolOuter)
|
|
407
|
+
if (!Number.isFinite(symbolOuter) || symbolOuter === 0) {
|
|
408
|
+
return null
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return SchematicConnectivityQaBuilder.#pinFinding(
|
|
412
|
+
'schematic.pin.endpoint-symbol',
|
|
413
|
+
'info',
|
|
414
|
+
pin,
|
|
415
|
+
{
|
|
416
|
+
symbolOuter
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Builds one pin finding row.
|
|
423
|
+
* @param {string} code Stable finding code.
|
|
424
|
+
* @param {'info' | 'warning'} severity Finding severity.
|
|
425
|
+
* @param {object} pin Normalized pin.
|
|
426
|
+
* @param {object} [details] Additional finding fields.
|
|
427
|
+
* @returns {object}
|
|
428
|
+
*/
|
|
429
|
+
static #pinFinding(code, severity, pin, details = {}) {
|
|
430
|
+
return SchematicConnectivityQaBuilder.#stripEmpty({
|
|
431
|
+
code,
|
|
432
|
+
severity,
|
|
433
|
+
ownerIndex: pin?.ownerIndex,
|
|
434
|
+
designator: pin?.designator,
|
|
435
|
+
name: pin?.name,
|
|
436
|
+
x: pin?.x,
|
|
437
|
+
y: pin?.y,
|
|
438
|
+
labelMode: pin?.labelMode,
|
|
439
|
+
...details
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Builds harness-specific connectivity findings.
|
|
445
|
+
* @param {object[]} sheetEntries Sheet entry records.
|
|
446
|
+
* @param {object | null} harnesses Harness read model.
|
|
447
|
+
* @returns {object[]}
|
|
448
|
+
*/
|
|
449
|
+
static #harnessFindings(sheetEntries, harnesses) {
|
|
450
|
+
return [
|
|
451
|
+
...SchematicConnectivityQaBuilder.#unresolvedHarnessTypeFindings(
|
|
452
|
+
sheetEntries,
|
|
453
|
+
harnesses
|
|
454
|
+
),
|
|
455
|
+
...SchematicConnectivityQaBuilder.#unlinkedHarnessEntryFindings(
|
|
456
|
+
harnesses
|
|
457
|
+
),
|
|
458
|
+
...SchematicConnectivityQaBuilder.#harnessTypeMismatchFindings(
|
|
459
|
+
harnesses
|
|
460
|
+
)
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Reports sheet entries that name a harness type not present locally.
|
|
466
|
+
* @param {object[]} sheetEntries Sheet entry records.
|
|
467
|
+
* @param {object | null} harnesses Harness read model.
|
|
468
|
+
* @returns {object[]}
|
|
469
|
+
*/
|
|
470
|
+
static #unresolvedHarnessTypeFindings(sheetEntries, harnesses) {
|
|
471
|
+
const knownTypes =
|
|
472
|
+
SchematicConnectivityQaBuilder.#knownHarnessTypes(harnesses)
|
|
473
|
+
|
|
474
|
+
return (sheetEntries || [])
|
|
475
|
+
.filter((entry) => {
|
|
476
|
+
const harnessType =
|
|
477
|
+
SchematicConnectivityQaBuilder.#normalizeHarnessType(
|
|
478
|
+
entry?.harnessType
|
|
479
|
+
)
|
|
480
|
+
return harnessType && !knownTypes.has(harnessType)
|
|
481
|
+
})
|
|
482
|
+
.map((entry) => ({
|
|
483
|
+
code: 'schematic.connectivity.harness-sheet-entry-unresolved-type',
|
|
484
|
+
severity: 'warning',
|
|
485
|
+
name: entry.name,
|
|
486
|
+
harnessType: entry.harnessType,
|
|
487
|
+
x: entry.x,
|
|
488
|
+
y: entry.y
|
|
489
|
+
}))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Reports harness entries that have no linked signal-harness geometry.
|
|
494
|
+
* @param {object | null} harnesses Harness read model.
|
|
495
|
+
* @returns {object[]}
|
|
496
|
+
*/
|
|
497
|
+
static #unlinkedHarnessEntryFindings(harnesses) {
|
|
498
|
+
const linksByConnector = new Map(
|
|
499
|
+
(harnesses?.bundleLinks || []).map((link) => [
|
|
500
|
+
link.connectorKey,
|
|
501
|
+
link
|
|
502
|
+
])
|
|
503
|
+
)
|
|
504
|
+
const findings = []
|
|
505
|
+
|
|
506
|
+
for (const connector of harnesses?.connectors || []) {
|
|
507
|
+
const link = linksByConnector.get(connector.key)
|
|
508
|
+
if (link?.signalHarnessKeys?.length) {
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const entry of connector.entries || []) {
|
|
513
|
+
findings.push({
|
|
514
|
+
code: 'schematic.connectivity.harness-entry-unlinked-signal',
|
|
515
|
+
severity: 'warning',
|
|
516
|
+
connectorKey: connector.key,
|
|
517
|
+
entryKey: entry.key,
|
|
518
|
+
name: entry.name,
|
|
519
|
+
harnessType: entry.harnessType
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return findings
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Reports connectors whose type label disagrees with entry harness types.
|
|
529
|
+
* @param {object | null} harnesses Harness read model.
|
|
530
|
+
* @returns {object[]}
|
|
531
|
+
*/
|
|
532
|
+
static #harnessTypeMismatchFindings(harnesses) {
|
|
533
|
+
const findings = []
|
|
534
|
+
|
|
535
|
+
for (const connector of harnesses?.connectors || []) {
|
|
536
|
+
const labelType =
|
|
537
|
+
SchematicConnectivityQaBuilder.#normalizeHarnessType(
|
|
538
|
+
connector.typeLabel?.text
|
|
539
|
+
)
|
|
540
|
+
if (!labelType) {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (const entry of connector.entries || []) {
|
|
545
|
+
const entryType =
|
|
546
|
+
SchematicConnectivityQaBuilder.#normalizeHarnessType(
|
|
547
|
+
entry.harnessType
|
|
548
|
+
)
|
|
549
|
+
if (!entryType || entryType === labelType) {
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
findings.push({
|
|
554
|
+
code: 'schematic.connectivity.harness-type-mismatch',
|
|
555
|
+
severity: 'warning',
|
|
556
|
+
connectorKey: connector.key,
|
|
557
|
+
entryKey: entry.key,
|
|
558
|
+
labelHarnessType: connector.typeLabel?.text,
|
|
559
|
+
entryHarnessType: entry.harnessType,
|
|
560
|
+
name: entry.name
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return findings
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Collects known local harness type names.
|
|
570
|
+
* @param {object | null} harnesses Harness read model.
|
|
571
|
+
* @returns {Set<string>}
|
|
572
|
+
*/
|
|
573
|
+
static #knownHarnessTypes(harnesses) {
|
|
574
|
+
const knownTypes = new Set()
|
|
575
|
+
|
|
576
|
+
for (const connector of harnesses?.connectors || []) {
|
|
577
|
+
SchematicConnectivityQaBuilder.#addHarnessType(
|
|
578
|
+
knownTypes,
|
|
579
|
+
connector.typeLabel?.text
|
|
580
|
+
)
|
|
581
|
+
for (const entry of connector.entries || []) {
|
|
582
|
+
SchematicConnectivityQaBuilder.#addHarnessType(
|
|
583
|
+
knownTypes,
|
|
584
|
+
entry.harnessType
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const link of harnesses?.bundleLinks || []) {
|
|
589
|
+
SchematicConnectivityQaBuilder.#addHarnessType(
|
|
590
|
+
knownTypes,
|
|
591
|
+
link.harnessType
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return knownTypes
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Adds one normalized harness type to a set.
|
|
600
|
+
* @param {Set<string>} knownTypes Known type set.
|
|
601
|
+
* @param {unknown} value Raw harness type.
|
|
602
|
+
* @returns {void}
|
|
603
|
+
*/
|
|
604
|
+
static #addHarnessType(knownTypes, value) {
|
|
605
|
+
const normalized =
|
|
606
|
+
SchematicConnectivityQaBuilder.#normalizeHarnessType(value)
|
|
607
|
+
if (normalized) {
|
|
608
|
+
knownTypes.add(normalized)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Normalizes one harness type for local comparisons.
|
|
614
|
+
* @param {unknown} value Raw harness type.
|
|
615
|
+
* @returns {string}
|
|
616
|
+
*/
|
|
617
|
+
static #normalizeHarnessType(value) {
|
|
618
|
+
return String(value || '')
|
|
619
|
+
.trim()
|
|
620
|
+
.toUpperCase()
|
|
621
|
+
}
|
|
622
|
+
|
|
225
623
|
/**
|
|
226
624
|
* Reports segment endpoints that touch another segment interior without an
|
|
227
625
|
* authored junction at the contact point.
|
|
@@ -308,6 +706,34 @@ export class SchematicConnectivityQaBuilder {
|
|
|
308
706
|
return findings
|
|
309
707
|
}
|
|
310
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Returns true when a pin name resembles a power rail label.
|
|
711
|
+
* @param {unknown} value Pin name.
|
|
712
|
+
* @returns {boolean}
|
|
713
|
+
*/
|
|
714
|
+
static #isPowerLikePinName(value) {
|
|
715
|
+
const name = String(value || '')
|
|
716
|
+
.trim()
|
|
717
|
+
.toUpperCase()
|
|
718
|
+
|
|
719
|
+
return (
|
|
720
|
+
/^(GND|AGND|DGND|PGND|VCC|VDD|VSS|VEE|VBAT|VBUS)$/u.test(name) ||
|
|
721
|
+
/^[+-]?\d+(?:\.\d+)?V(?:_[A-Z0-9]+)?$/u.test(name) ||
|
|
722
|
+
/^V(?:IN|OUT|REF|CORE|IO|A|D|P)(?:_[A-Z0-9]+)?$/u.test(name)
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Returns true when an electrical type code represents a power pin.
|
|
728
|
+
* @param {unknown} value Electrical type code.
|
|
729
|
+
* @returns {boolean}
|
|
730
|
+
*/
|
|
731
|
+
static #isPowerElectricalType(value) {
|
|
732
|
+
const electrical = Number(value)
|
|
733
|
+
|
|
734
|
+
return electrical === 7 || electrical === 8
|
|
735
|
+
}
|
|
736
|
+
|
|
311
737
|
/**
|
|
312
738
|
* Counts findings with one code.
|
|
313
739
|
* @param {object[]} findings QA findings.
|
|
@@ -318,6 +744,20 @@ export class SchematicConnectivityQaBuilder {
|
|
|
318
744
|
return findings.filter((finding) => finding.code === code).length
|
|
319
745
|
}
|
|
320
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Removes empty fields while preserving zeros and false.
|
|
749
|
+
* @param {Record<string, unknown>} row Input row.
|
|
750
|
+
* @returns {Record<string, unknown>}
|
|
751
|
+
*/
|
|
752
|
+
static #stripEmpty(row) {
|
|
753
|
+
return Object.fromEntries(
|
|
754
|
+
Object.entries(row || {}).filter(([, value]) => {
|
|
755
|
+
if (Array.isArray(value)) return value.length > 0
|
|
756
|
+
return value !== undefined && value !== null && value !== ''
|
|
757
|
+
})
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
|
|
321
761
|
/**
|
|
322
762
|
* Compares text rows by text and point.
|
|
323
763
|
* @param {object} left First row.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
6
|
+
'image/png',
|
|
7
|
+
'image/jpeg',
|
|
8
|
+
'image/gif',
|
|
9
|
+
'image/svg+xml',
|
|
10
|
+
'image/webp',
|
|
11
|
+
'image/bmp'
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds deterministic diagnostics for normalized schematic image rows.
|
|
16
|
+
*/
|
|
17
|
+
export class SchematicImageDiagnosticsBuilder {
|
|
18
|
+
static SCHEMA = 'altium-toolkit.schematic.image-diagnostics.a1'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds a schematic image diagnostics report.
|
|
22
|
+
* @param {object} input Parser root, schematic model, or options object.
|
|
23
|
+
* @returns {object}
|
|
24
|
+
*/
|
|
25
|
+
static build(input = {}) {
|
|
26
|
+
const images = SchematicImageDiagnosticsBuilder.#images(input)
|
|
27
|
+
const rows = images.map((image, index) =>
|
|
28
|
+
SchematicImageDiagnosticsBuilder.#imageRow(image, index)
|
|
29
|
+
)
|
|
30
|
+
const findings = rows.flatMap((row) =>
|
|
31
|
+
SchematicImageDiagnosticsBuilder.#findings(row)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
schema: SchematicImageDiagnosticsBuilder.SCHEMA,
|
|
36
|
+
summary: SchematicImageDiagnosticsBuilder.#summary(rows, findings),
|
|
37
|
+
images: rows,
|
|
38
|
+
findings
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves image rows from a parser root or direct schematic payload.
|
|
44
|
+
* @param {object} input Parser root, schematic model, or options object.
|
|
45
|
+
* @returns {object[]}
|
|
46
|
+
*/
|
|
47
|
+
static #images(input) {
|
|
48
|
+
if (Array.isArray(input?.images)) {
|
|
49
|
+
return input.images
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(input?.schematic?.images)) {
|
|
52
|
+
return input.schematic.images
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds one normalized image diagnostic row.
|
|
60
|
+
* @param {object} image Normalized image row.
|
|
61
|
+
* @param {number} index Image index.
|
|
62
|
+
* @returns {object}
|
|
63
|
+
*/
|
|
64
|
+
static #imageRow(image, index) {
|
|
65
|
+
const mimeType = String(image?.mimeType || '').trim()
|
|
66
|
+
const sourceMimeType = String(image?.sourceMimeType || '').trim()
|
|
67
|
+
const hasPayload = String(image?.dataBase64 || '').length > 0
|
|
68
|
+
const embedded = image?.embedded === true
|
|
69
|
+
const state =
|
|
70
|
+
image?.diagnosticState ||
|
|
71
|
+
(embedded
|
|
72
|
+
? hasPayload
|
|
73
|
+
? 'embedded'
|
|
74
|
+
: 'missing-embedded-payload'
|
|
75
|
+
: 'external')
|
|
76
|
+
|
|
77
|
+
return SchematicImageDiagnosticsBuilder.#stripEmpty({
|
|
78
|
+
key: 'schematic-image-' + index,
|
|
79
|
+
index,
|
|
80
|
+
fileName: image?.fileName,
|
|
81
|
+
embedded,
|
|
82
|
+
diagnosticState: state,
|
|
83
|
+
mimeType,
|
|
84
|
+
sourceMimeType,
|
|
85
|
+
hasPayload,
|
|
86
|
+
hasAlpha: image?.hasAlpha === true,
|
|
87
|
+
payloadStatus:
|
|
88
|
+
embedded && !hasPayload
|
|
89
|
+
? 'missing'
|
|
90
|
+
: hasPayload
|
|
91
|
+
? 'available'
|
|
92
|
+
: 'external',
|
|
93
|
+
mimeStatus:
|
|
94
|
+
hasPayload &&
|
|
95
|
+
(!mimeType || !SUPPORTED_IMAGE_MIME_TYPES.has(mimeType))
|
|
96
|
+
? 'unsupported'
|
|
97
|
+
: mimeType
|
|
98
|
+
? 'supported'
|
|
99
|
+
: 'unknown',
|
|
100
|
+
converted:
|
|
101
|
+
Boolean(sourceMimeType && mimeType) &&
|
|
102
|
+
sourceMimeType !== mimeType
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds diagnostic findings for one image row.
|
|
108
|
+
* @param {object} row Normalized image diagnostic row.
|
|
109
|
+
* @returns {object[]}
|
|
110
|
+
*/
|
|
111
|
+
static #findings(row) {
|
|
112
|
+
const findings = []
|
|
113
|
+
|
|
114
|
+
if (row.converted) {
|
|
115
|
+
findings.push(
|
|
116
|
+
SchematicImageDiagnosticsBuilder.#finding(
|
|
117
|
+
'schematic.image.converted-payload',
|
|
118
|
+
'info',
|
|
119
|
+
row
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
if (row.diagnosticState === 'external') {
|
|
124
|
+
findings.push(
|
|
125
|
+
SchematicImageDiagnosticsBuilder.#finding(
|
|
126
|
+
'schematic.image.external-reference',
|
|
127
|
+
'info',
|
|
128
|
+
row
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
if (row.payloadStatus === 'missing') {
|
|
133
|
+
findings.push(
|
|
134
|
+
SchematicImageDiagnosticsBuilder.#finding(
|
|
135
|
+
'schematic.image.missing-embedded-payload',
|
|
136
|
+
'warning',
|
|
137
|
+
row
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (row.mimeStatus === 'unsupported') {
|
|
142
|
+
findings.push(
|
|
143
|
+
SchematicImageDiagnosticsBuilder.#finding(
|
|
144
|
+
'schematic.image.unsupported-mime-type',
|
|
145
|
+
'warning',
|
|
146
|
+
row
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return findings
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Builds the report summary.
|
|
156
|
+
* @param {object[]} rows Normalized image rows.
|
|
157
|
+
* @param {object[]} findings Diagnostic findings.
|
|
158
|
+
* @returns {object}
|
|
159
|
+
*/
|
|
160
|
+
static #summary(rows, findings) {
|
|
161
|
+
return {
|
|
162
|
+
imageCount: rows.length,
|
|
163
|
+
embeddedImageCount: rows.filter((row) => row.embedded).length,
|
|
164
|
+
embeddedPayloadCount: rows.filter(
|
|
165
|
+
(row) => row.embedded && row.hasPayload
|
|
166
|
+
).length,
|
|
167
|
+
externalReferenceCount: rows.filter(
|
|
168
|
+
(row) => row.diagnosticState === 'external'
|
|
169
|
+
).length,
|
|
170
|
+
missingPayloadCount: rows.filter(
|
|
171
|
+
(row) => row.payloadStatus === 'missing'
|
|
172
|
+
).length,
|
|
173
|
+
unsupportedMimeTypeCount: rows.filter(
|
|
174
|
+
(row) => row.mimeStatus === 'unsupported'
|
|
175
|
+
).length,
|
|
176
|
+
convertedPayloadCount: rows.filter((row) => row.converted).length,
|
|
177
|
+
alphaPayloadCount: rows.filter((row) => row.hasAlpha).length,
|
|
178
|
+
findingCount: findings.length
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Builds one image finding row.
|
|
184
|
+
* @param {string} code Stable finding code.
|
|
185
|
+
* @param {'info' | 'warning'} severity Finding severity.
|
|
186
|
+
* @param {object} row Image diagnostic row.
|
|
187
|
+
* @returns {object}
|
|
188
|
+
*/
|
|
189
|
+
static #finding(code, severity, row) {
|
|
190
|
+
return SchematicImageDiagnosticsBuilder.#stripEmpty({
|
|
191
|
+
code,
|
|
192
|
+
severity,
|
|
193
|
+
imageKey: row.key,
|
|
194
|
+
fileName: row.fileName,
|
|
195
|
+
diagnosticState: row.diagnosticState,
|
|
196
|
+
mimeType: row.mimeType,
|
|
197
|
+
sourceMimeType: row.sourceMimeType
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes empty fields while preserving zeros and false.
|
|
203
|
+
* @param {Record<string, unknown>} row Input row.
|
|
204
|
+
* @returns {Record<string, unknown>}
|
|
205
|
+
*/
|
|
206
|
+
static #stripEmpty(row) {
|
|
207
|
+
return Object.fromEntries(
|
|
208
|
+
Object.entries(row || {}).filter(([, value]) => {
|
|
209
|
+
if (Array.isArray(value)) return value.length > 0
|
|
210
|
+
return value !== undefined && value !== null && value !== ''
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|