altium-toolkit 1.1.23 → 1.1.25
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 +34 -13
- package/docs/model-format.md +37 -8
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +8 -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/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 +7 -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 UnsupportedFeatureReportBuilder {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Builds an unsupported feature report.
|
|
13
|
-
* @param {{ models?: object[], recordTypes?: object[], rawRecords?: object[], opaqueRecords?: object[], diagnostics?: object[] }} [input] Report input.
|
|
13
|
+
* @param {{ models?: object[], recordTypes?: object[], rawRecords?: object[], opaqueRecords?: object[], diagnostics?: object[], edgeCases?: object[] }} [input] Report input.
|
|
14
14
|
* @returns {object}
|
|
15
15
|
*/
|
|
16
16
|
static build(input = {}) {
|
|
@@ -32,11 +32,16 @@ export class UnsupportedFeatureReportBuilder {
|
|
|
32
32
|
input,
|
|
33
33
|
models
|
|
34
34
|
)
|
|
35
|
+
const edgeCases = UnsupportedFeatureReportBuilder.#edgeCases(
|
|
36
|
+
input,
|
|
37
|
+
models
|
|
38
|
+
)
|
|
35
39
|
const itemCount =
|
|
36
40
|
recordTypes.length +
|
|
37
41
|
rawRecords.length +
|
|
38
42
|
opaqueRecords.length +
|
|
39
|
-
diagnostics.length
|
|
43
|
+
diagnostics.length +
|
|
44
|
+
edgeCases.length
|
|
40
45
|
|
|
41
46
|
return {
|
|
42
47
|
schema: UnsupportedFeatureReportBuilder.SCHEMA,
|
|
@@ -46,13 +51,15 @@ export class UnsupportedFeatureReportBuilder {
|
|
|
46
51
|
rawRecordCount: rawRecords.length,
|
|
47
52
|
opaqueRecordCount: opaqueRecords.length,
|
|
48
53
|
diagnosticCount: diagnostics.length,
|
|
54
|
+
edgeCaseCount: edgeCases.length,
|
|
49
55
|
itemCount,
|
|
50
56
|
status: itemCount ? 'unsupported' : 'supported'
|
|
51
57
|
},
|
|
52
58
|
recordTypes,
|
|
53
59
|
rawRecords,
|
|
54
60
|
opaqueRecords,
|
|
55
|
-
diagnostics
|
|
61
|
+
diagnostics,
|
|
62
|
+
edgeCases
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -344,6 +351,506 @@ export class UnsupportedFeatureReportBuilder {
|
|
|
344
351
|
)
|
|
345
352
|
}
|
|
346
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Collects parser edge-case coverage rows.
|
|
356
|
+
* @param {object} input Report input.
|
|
357
|
+
* @param {object[]} models Parser roots.
|
|
358
|
+
* @returns {object[]}
|
|
359
|
+
*/
|
|
360
|
+
static #edgeCases(input, models) {
|
|
361
|
+
return UnsupportedFeatureReportBuilder.#dedupeEdgeCases(
|
|
362
|
+
[
|
|
363
|
+
...(Array.isArray(input?.edgeCases)
|
|
364
|
+
? input.edgeCases.map((edgeCase) => ({
|
|
365
|
+
edgeCase: edgeCase || {},
|
|
366
|
+
fileName: edgeCase?.fileName || '',
|
|
367
|
+
domain: edgeCase?.domain || ''
|
|
368
|
+
}))
|
|
369
|
+
: []),
|
|
370
|
+
...models.flatMap((model) =>
|
|
371
|
+
(model?.diagnostics || [])
|
|
372
|
+
.filter((diagnostic) =>
|
|
373
|
+
UnsupportedFeatureReportBuilder.#isEdgeCaseDiagnostic(
|
|
374
|
+
diagnostic
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
.map((diagnostic) => ({
|
|
378
|
+
edgeCase: diagnostic,
|
|
379
|
+
fileName:
|
|
380
|
+
model.fileName || diagnostic.fileName || '',
|
|
381
|
+
domain: diagnostic.domain || ''
|
|
382
|
+
}))
|
|
383
|
+
),
|
|
384
|
+
...models.flatMap((model) =>
|
|
385
|
+
UnsupportedFeatureReportBuilder.#modelDerivedEdgeCases(
|
|
386
|
+
model
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
].map(({ edgeCase, fileName, domain }) =>
|
|
390
|
+
UnsupportedFeatureReportBuilder.#stripUndefined({
|
|
391
|
+
fileName,
|
|
392
|
+
domain,
|
|
393
|
+
code: edgeCase.code,
|
|
394
|
+
feature: edgeCase.feature,
|
|
395
|
+
supportState: edgeCase.supportState,
|
|
396
|
+
severity: edgeCase.severity,
|
|
397
|
+
message: edgeCase.message,
|
|
398
|
+
source: edgeCase.source,
|
|
399
|
+
sourceStream: edgeCase.sourceStream,
|
|
400
|
+
sourceStorage: edgeCase.sourceStorage,
|
|
401
|
+
recordIndex:
|
|
402
|
+
UnsupportedFeatureReportBuilder.#finiteOrUndefined(
|
|
403
|
+
edgeCase.recordIndex
|
|
404
|
+
),
|
|
405
|
+
recordType:
|
|
406
|
+
UnsupportedFeatureReportBuilder.#finiteOrUndefined(
|
|
407
|
+
edgeCase.recordType
|
|
408
|
+
),
|
|
409
|
+
errorKind: edgeCase.errorKind
|
|
410
|
+
})
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Collects compatibility edge cases derivable from parsed model data.
|
|
417
|
+
* @param {object} model Parser root.
|
|
418
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
419
|
+
*/
|
|
420
|
+
static #modelDerivedEdgeCases(model) {
|
|
421
|
+
const fileName = String(model?.fileName || '')
|
|
422
|
+
const pcb = model?.pcb || {}
|
|
423
|
+
|
|
424
|
+
return [
|
|
425
|
+
...UnsupportedFeatureReportBuilder.#padEdgeCases(
|
|
426
|
+
pcb.pads,
|
|
427
|
+
fileName
|
|
428
|
+
),
|
|
429
|
+
...UnsupportedFeatureReportBuilder.#arcEdgeCases(
|
|
430
|
+
pcb.arcs,
|
|
431
|
+
fileName
|
|
432
|
+
),
|
|
433
|
+
...UnsupportedFeatureReportBuilder.#regionEdgeCases(
|
|
434
|
+
pcb.regions,
|
|
435
|
+
fileName
|
|
436
|
+
),
|
|
437
|
+
...UnsupportedFeatureReportBuilder.#componentBodyEdgeCases(
|
|
438
|
+
pcb.componentBodies,
|
|
439
|
+
fileName
|
|
440
|
+
),
|
|
441
|
+
...UnsupportedFeatureReportBuilder.#embeddedModelIntegrityEdgeCases(
|
|
442
|
+
pcb.embeddedModelIntegrity?.issues,
|
|
443
|
+
fileName
|
|
444
|
+
)
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Collects pad compatibility rows.
|
|
450
|
+
* @param {object[] | undefined} pads Pads.
|
|
451
|
+
* @param {string} fileName Source file.
|
|
452
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
453
|
+
*/
|
|
454
|
+
static #padEdgeCases(pads, fileName) {
|
|
455
|
+
return (Array.isArray(pads) ? pads : []).flatMap((pad) => {
|
|
456
|
+
const rows = []
|
|
457
|
+
|
|
458
|
+
if (UnsupportedFeatureReportBuilder.#hasOctagonalPadShape(pad)) {
|
|
459
|
+
rows.push(
|
|
460
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
461
|
+
code: 'pcb.pad.octagonal',
|
|
462
|
+
feature: 'octagonal-pad',
|
|
463
|
+
supportState: 'inspection-required',
|
|
464
|
+
severity: 'warning',
|
|
465
|
+
message:
|
|
466
|
+
'Pad uses an octagonal shape that should be inspected in downstream views.',
|
|
467
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
468
|
+
})
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (Number(pad?.padMode) > 0) {
|
|
473
|
+
rows.push(
|
|
474
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
475
|
+
code: 'pcb.pad.non-simple-stack',
|
|
476
|
+
feature: 'non-simple-pad-stack',
|
|
477
|
+
supportState: 'inspection-required',
|
|
478
|
+
severity: 'warning',
|
|
479
|
+
message:
|
|
480
|
+
'Pad uses layer-specific pad-stack mode metadata.',
|
|
481
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (
|
|
487
|
+
UnsupportedFeatureReportBuilder.#hasLayerSpecificPadGeometry(
|
|
488
|
+
pad
|
|
489
|
+
)
|
|
490
|
+
) {
|
|
491
|
+
rows.push(
|
|
492
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
493
|
+
code: 'pcb.pad.layer-specific-geometry',
|
|
494
|
+
feature: 'layer-specific-pad-geometry',
|
|
495
|
+
supportState: 'preserved',
|
|
496
|
+
severity: 'info',
|
|
497
|
+
message:
|
|
498
|
+
'Pad has different top, middle, or bottom geometry.',
|
|
499
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
500
|
+
})
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (UnsupportedFeatureReportBuilder.#hasSlotHole(pad)) {
|
|
505
|
+
rows.push(
|
|
506
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
507
|
+
code: 'pcb.pad.slot-hole',
|
|
508
|
+
feature: 'slot-hole',
|
|
509
|
+
supportState: 'preserved',
|
|
510
|
+
severity: 'info',
|
|
511
|
+
message: 'Pad uses slot-hole geometry.',
|
|
512
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
513
|
+
})
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (UnsupportedFeatureReportBuilder.#hasManualMaskPaste(pad)) {
|
|
518
|
+
rows.push(
|
|
519
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
520
|
+
code: 'pcb.pad.manual-mask-paste',
|
|
521
|
+
feature: 'manual-mask-paste-expansion',
|
|
522
|
+
supportState: 'inspection-required',
|
|
523
|
+
severity: 'warning',
|
|
524
|
+
message:
|
|
525
|
+
'Pad carries manual solder-mask or paste-mask expansion metadata.',
|
|
526
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
527
|
+
})
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (
|
|
532
|
+
UnsupportedFeatureReportBuilder.#hasMissingCustomShapeGeometry(
|
|
533
|
+
pad
|
|
534
|
+
)
|
|
535
|
+
) {
|
|
536
|
+
rows.push(
|
|
537
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
538
|
+
code: 'pcb.custom-shape.missing-linked-geometry',
|
|
539
|
+
feature: 'custom-pad-shape-linkage',
|
|
540
|
+
supportState: 'inspection-required',
|
|
541
|
+
severity: 'warning',
|
|
542
|
+
message:
|
|
543
|
+
'Custom pad shape sidecar has no linked primitive geometry.',
|
|
544
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(pad)
|
|
545
|
+
})
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return rows
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Collects arc compatibility rows.
|
|
555
|
+
* @param {object[] | undefined} arcs Arcs.
|
|
556
|
+
* @param {string} fileName Source file.
|
|
557
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
558
|
+
*/
|
|
559
|
+
static #arcEdgeCases(arcs, fileName) {
|
|
560
|
+
return (Array.isArray(arcs) ? arcs : [])
|
|
561
|
+
.filter((arc) => {
|
|
562
|
+
const radius = Math.abs(Number(arc?.radius))
|
|
563
|
+
const width = Math.abs(Number(arc?.width))
|
|
564
|
+
return Number.isFinite(radius) && radius > 0 && width >= radius
|
|
565
|
+
})
|
|
566
|
+
.map((arc) =>
|
|
567
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
568
|
+
code: 'pcb.arc.width-radius-conflict',
|
|
569
|
+
feature: 'wide-arc',
|
|
570
|
+
supportState: 'inspection-required',
|
|
571
|
+
severity: 'warning',
|
|
572
|
+
message:
|
|
573
|
+
'Arc stroke width is greater than or equal to its radius.',
|
|
574
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(arc)
|
|
575
|
+
})
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Collects region compatibility rows.
|
|
581
|
+
* @param {object[] | undefined} regions Regions.
|
|
582
|
+
* @param {string} fileName Source file.
|
|
583
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
584
|
+
*/
|
|
585
|
+
static #regionEdgeCases(regions, fileName) {
|
|
586
|
+
return (Array.isArray(regions) ? regions : [])
|
|
587
|
+
.filter(
|
|
588
|
+
(region) =>
|
|
589
|
+
Number(region?.holeCount || region?.HOLECOUNT || 0) > 0
|
|
590
|
+
)
|
|
591
|
+
.map((region) =>
|
|
592
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
593
|
+
code: 'pcb.region.holes',
|
|
594
|
+
feature: 'region-with-holes',
|
|
595
|
+
supportState: 'preserved',
|
|
596
|
+
severity: 'info',
|
|
597
|
+
message: 'Region contains one or more holes.',
|
|
598
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(region)
|
|
599
|
+
})
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Collects component-body compatibility rows.
|
|
605
|
+
* @param {object[] | undefined} componentBodies Component bodies.
|
|
606
|
+
* @param {string} fileName Source file.
|
|
607
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
608
|
+
*/
|
|
609
|
+
static #componentBodyEdgeCases(componentBodies, fileName) {
|
|
610
|
+
return (Array.isArray(componentBodies) ? componentBodies : [])
|
|
611
|
+
.filter((componentBody) =>
|
|
612
|
+
UnsupportedFeatureReportBuilder.#missingStaticBodyGeometry(
|
|
613
|
+
componentBody
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
.map((componentBody) =>
|
|
617
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
618
|
+
code: 'pcb.model.shape-body-static-geometry-missing',
|
|
619
|
+
feature: 'shape-based-3d-body',
|
|
620
|
+
supportState: 'inspection-required',
|
|
621
|
+
severity: 'warning',
|
|
622
|
+
message:
|
|
623
|
+
'Shape-based 3D body has no complete static geometry description.',
|
|
624
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(
|
|
625
|
+
componentBody
|
|
626
|
+
)
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Collects embedded-model integrity rows.
|
|
633
|
+
* @param {object[] | undefined} issues Integrity issues.
|
|
634
|
+
* @param {string} fileName Source file.
|
|
635
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }[]}
|
|
636
|
+
*/
|
|
637
|
+
static #embeddedModelIntegrityEdgeCases(issues, fileName) {
|
|
638
|
+
return (Array.isArray(issues) ? issues : []).map((issue) =>
|
|
639
|
+
UnsupportedFeatureReportBuilder.#pcbEdgeCase(fileName, {
|
|
640
|
+
code: issue.code,
|
|
641
|
+
feature: 'embedded-model-integrity',
|
|
642
|
+
supportState: 'diagnostic',
|
|
643
|
+
severity: issue.severity || 'warning',
|
|
644
|
+
message: issue.message,
|
|
645
|
+
...UnsupportedFeatureReportBuilder.#recordLocation(issue)
|
|
646
|
+
})
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Wraps one PCB edge-case row.
|
|
652
|
+
* @param {string} fileName Source file.
|
|
653
|
+
* @param {object} edgeCase Edge-case row.
|
|
654
|
+
* @returns {{ edgeCase: object, fileName: string, domain: string }}
|
|
655
|
+
*/
|
|
656
|
+
static #pcbEdgeCase(fileName, edgeCase) {
|
|
657
|
+
return {
|
|
658
|
+
edgeCase,
|
|
659
|
+
fileName,
|
|
660
|
+
domain: 'pcb'
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Extracts common record location fields.
|
|
666
|
+
* @param {object} row Source row.
|
|
667
|
+
* @returns {object}
|
|
668
|
+
*/
|
|
669
|
+
static #recordLocation(row) {
|
|
670
|
+
return UnsupportedFeatureReportBuilder.#stripUndefined({
|
|
671
|
+
source: row?.source,
|
|
672
|
+
sourceStream: row?.sourceStream,
|
|
673
|
+
sourceStorage: row?.sourceStorage,
|
|
674
|
+
recordIndex: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
|
|
675
|
+
row?.recordIndex
|
|
676
|
+
),
|
|
677
|
+
recordType: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
|
|
678
|
+
row?.recordType
|
|
679
|
+
)
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Returns true when one pad uses an octagonal pad shape.
|
|
685
|
+
* @param {object} pad Pad row.
|
|
686
|
+
* @returns {boolean}
|
|
687
|
+
*/
|
|
688
|
+
static #hasOctagonalPadShape(pad) {
|
|
689
|
+
const shapeNames = [
|
|
690
|
+
pad?.shapeTopName,
|
|
691
|
+
pad?.shapeMidName,
|
|
692
|
+
pad?.shapeBottomName,
|
|
693
|
+
pad?.padShapeNames?.top,
|
|
694
|
+
pad?.padShapeNames?.middle,
|
|
695
|
+
pad?.padShapeNames?.bottom
|
|
696
|
+
].map((value) => String(value || '').toLowerCase())
|
|
697
|
+
const shapeCodes = [pad?.shapeTop, pad?.shapeMid, pad?.shapeBottom].map(
|
|
698
|
+
(value) => Number(value)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
shapeNames.includes('octagonal') || shapeCodes.includes(Number(3))
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Returns true when top, middle, or bottom pad geometry differs.
|
|
708
|
+
* @param {object} pad Pad row.
|
|
709
|
+
* @returns {boolean}
|
|
710
|
+
*/
|
|
711
|
+
static #hasLayerSpecificPadGeometry(pad) {
|
|
712
|
+
const sizes = [
|
|
713
|
+
[pad?.sizeTopX, pad?.sizeTopY],
|
|
714
|
+
[pad?.sizeMidX, pad?.sizeMidY],
|
|
715
|
+
[pad?.sizeBottomX, pad?.sizeBottomY]
|
|
716
|
+
].map(([x, y]) => [Number(x), Number(y)])
|
|
717
|
+
const finiteSizes = sizes.filter(([x, y]) =>
|
|
718
|
+
[x, y].every(Number.isFinite)
|
|
719
|
+
)
|
|
720
|
+
const shapes = [pad?.shapeTop, pad?.shapeMid, pad?.shapeBottom]
|
|
721
|
+
.map((value) => Number(value))
|
|
722
|
+
.filter(Number.isFinite)
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
UnsupportedFeatureReportBuilder.#hasDistinctPairs(finiteSizes) ||
|
|
726
|
+
new Set(shapes).size > 1
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Returns true when an array of numeric pairs has more than one value.
|
|
732
|
+
* @param {number[][]} pairs Numeric pairs.
|
|
733
|
+
* @returns {boolean}
|
|
734
|
+
*/
|
|
735
|
+
static #hasDistinctPairs(pairs) {
|
|
736
|
+
return new Set(pairs.map((pair) => pair.join(','))).size > 1
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Returns true when one pad uses slot-hole geometry.
|
|
741
|
+
* @param {object} pad Pad row.
|
|
742
|
+
* @returns {boolean}
|
|
743
|
+
*/
|
|
744
|
+
static #hasSlotHole(pad) {
|
|
745
|
+
return (
|
|
746
|
+
String(
|
|
747
|
+
pad?.holeShapeName || pad?.holeGeometry?.shapeName || ''
|
|
748
|
+
).toLowerCase() === 'slot'
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Returns true when one pad uses manual mask or paste expansion metadata.
|
|
754
|
+
* @param {object} pad Pad row.
|
|
755
|
+
* @returns {boolean}
|
|
756
|
+
*/
|
|
757
|
+
static #hasManualMaskPaste(pad) {
|
|
758
|
+
const sources = [
|
|
759
|
+
pad?.pasteMaskExpansionSource,
|
|
760
|
+
pad?.solderMaskExpansionSource
|
|
761
|
+
].map((value) => String(value || '').toLowerCase())
|
|
762
|
+
const modes = [
|
|
763
|
+
pad?.pasteMaskExpansionMode,
|
|
764
|
+
pad?.solderMaskExpansionMode
|
|
765
|
+
].map((value) => Number(value))
|
|
766
|
+
|
|
767
|
+
return sources.includes('manual') || modes.includes(Number(2))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Returns true when custom-shape layers have no linked geometry.
|
|
772
|
+
* @param {object} pad Pad row.
|
|
773
|
+
* @returns {boolean}
|
|
774
|
+
*/
|
|
775
|
+
static #hasMissingCustomShapeGeometry(pad) {
|
|
776
|
+
const layers = Array.isArray(pad?.customShape?.layers)
|
|
777
|
+
? pad.customShape.layers
|
|
778
|
+
: []
|
|
779
|
+
|
|
780
|
+
return (
|
|
781
|
+
layers.length > 0 &&
|
|
782
|
+
layers.some(
|
|
783
|
+
(layer) =>
|
|
784
|
+
!UnsupportedFeatureReportBuilder.#customShapeLayerHasGeometry(
|
|
785
|
+
layer
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Returns true when a custom-shape layer links to any primitive geometry.
|
|
793
|
+
* @param {object} layer Custom shape layer.
|
|
794
|
+
* @returns {boolean}
|
|
795
|
+
*/
|
|
796
|
+
static #customShapeLayerHasGeometry(layer) {
|
|
797
|
+
return ['regions', 'shapeRegions', 'arcs', 'tracks', 'fills'].some(
|
|
798
|
+
(key) => Array.isArray(layer?.[key]) && layer[key].length > 0
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Returns true when a known shape body lacks complete static geometry.
|
|
804
|
+
* @param {object} componentBody Component body.
|
|
805
|
+
* @returns {boolean}
|
|
806
|
+
*/
|
|
807
|
+
static #missingStaticBodyGeometry(componentBody) {
|
|
808
|
+
const typeName = String(componentBody?.modelTypeName || '')
|
|
809
|
+
const isShapeBody = [
|
|
810
|
+
'extruded-polygon',
|
|
811
|
+
'cone',
|
|
812
|
+
'cylinder',
|
|
813
|
+
'sphere'
|
|
814
|
+
].includes(typeName)
|
|
815
|
+
|
|
816
|
+
if (!isShapeBody) {
|
|
817
|
+
return false
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return componentBody?.staticGeometry?.status !== 'complete'
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Removes repeated edge-case rows.
|
|
825
|
+
* @param {object[]} edgeCases Edge cases.
|
|
826
|
+
* @returns {object[]}
|
|
827
|
+
*/
|
|
828
|
+
static #dedupeEdgeCases(edgeCases) {
|
|
829
|
+
const seen = new Set()
|
|
830
|
+
const deduped = []
|
|
831
|
+
|
|
832
|
+
for (const edgeCase of edgeCases) {
|
|
833
|
+
const row = edgeCase?.edgeCase || edgeCase || {}
|
|
834
|
+
const key = [
|
|
835
|
+
edgeCase?.fileName || row.fileName || '',
|
|
836
|
+
edgeCase?.domain || row.domain || '',
|
|
837
|
+
row.code || '',
|
|
838
|
+
row.sourceStream || '',
|
|
839
|
+
row.sourceStorage || '',
|
|
840
|
+
row.recordIndex ?? ''
|
|
841
|
+
].join('\u0000')
|
|
842
|
+
|
|
843
|
+
if (seen.has(key)) {
|
|
844
|
+
continue
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
seen.add(key)
|
|
848
|
+
deduped.push(edgeCase)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return deduped
|
|
852
|
+
}
|
|
853
|
+
|
|
347
854
|
/**
|
|
348
855
|
* Returns true for unsupported-feature diagnostics.
|
|
349
856
|
* @param {object} diagnostic Diagnostic row.
|
|
@@ -357,6 +864,19 @@ export class UnsupportedFeatureReportBuilder {
|
|
|
357
864
|
.includes('unsupported')
|
|
358
865
|
}
|
|
359
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Returns true for diagnostics that describe parser edge-case coverage.
|
|
869
|
+
* @param {object} diagnostic Diagnostic row.
|
|
870
|
+
* @returns {boolean}
|
|
871
|
+
*/
|
|
872
|
+
static #isEdgeCaseDiagnostic(diagnostic) {
|
|
873
|
+
if (!diagnostic || typeof diagnostic !== 'object') return false
|
|
874
|
+
if (diagnostic.errorKind === 'edge-case') return true
|
|
875
|
+
return String(diagnostic.code || '')
|
|
876
|
+
.toLowerCase()
|
|
877
|
+
.includes('edge-case')
|
|
878
|
+
}
|
|
879
|
+
|
|
360
880
|
/**
|
|
361
881
|
* Returns a finite number or undefined.
|
|
362
882
|
* @param {unknown} value Candidate value.
|
package/src/parser.mjs
CHANGED
|
@@ -68,9 +68,11 @@ export { PcbComponentKindPolicy } from './core/altium/PcbComponentKindPolicy.mjs
|
|
|
68
68
|
export { PcbComponentPrimitiveIndexer } from './core/altium/PcbComponentPrimitiveIndexer.mjs'
|
|
69
69
|
export { PcbCustomPadShapeParser } from './core/altium/PcbCustomPadShapeParser.mjs'
|
|
70
70
|
export { PcbDimensionParser } from './core/altium/PcbDimensionParser.mjs'
|
|
71
|
+
export { PcbDimensionReportBuilder } from './core/altium/PcbDimensionReportBuilder.mjs'
|
|
71
72
|
export { PcbEmbeddedFontExtractor } from './core/altium/PcbEmbeddedFontExtractor.mjs'
|
|
72
73
|
export { PcbEmbeddedModelExtractor } from './core/altium/PcbEmbeddedModelExtractor.mjs'
|
|
73
74
|
export { PcbExtendedPrimitiveInformationParser } from './core/altium/PcbExtendedPrimitiveInformationParser.mjs'
|
|
75
|
+
export { PcbFabricationReadinessReportBuilder } from './core/altium/PcbFabricationReadinessReportBuilder.mjs'
|
|
74
76
|
export { PcbFontMetricsParser } from './core/altium/PcbFontMetricsParser.mjs'
|
|
75
77
|
export { PcbLayerIdCodec } from './core/altium/PcbLayerIdCodec.mjs'
|
|
76
78
|
export { PcbLayerGroups } from './core/altium/PcbLayerGroups.mjs'
|
|
@@ -89,12 +91,15 @@ export { PcbPadStackParser } from './core/altium/PcbPadStackParser.mjs'
|
|
|
89
91
|
export { PcbPlacedFootprintManifestBuilder } from './core/altium/PcbPlacedFootprintManifestBuilder.mjs'
|
|
90
92
|
export { PcbPickPlacePositionResolver } from './core/altium/PcbPickPlacePositionResolver.mjs'
|
|
91
93
|
export { PcbPolygonRecordParser } from './core/altium/PcbPolygonRecordParser.mjs'
|
|
94
|
+
export { PcbPrimitiveFieldOffsetReportBuilder } from './core/altium/PcbPrimitiveFieldOffsetReportBuilder.mjs'
|
|
92
95
|
export { PcbRawRecordRegistry } from './core/altium/PcbRawRecordRegistry.mjs'
|
|
93
96
|
export { PcbReviewMetadataBuilder } from './core/altium/PcbReviewMetadataBuilder.mjs'
|
|
97
|
+
export { PcbReviewPolygonRealizationBuilder } from './core/altium/PcbReviewPolygonRealizationBuilder.mjs'
|
|
94
98
|
export { PcbClassReportBuilder } from './core/altium/PcbClassReportBuilder.mjs'
|
|
95
99
|
export { PcbInspectionReportBuilder } from './core/altium/PcbInspectionReportBuilder.mjs'
|
|
96
100
|
export { PcbNetMembershipReportBuilder } from './core/altium/PcbNetMembershipReportBuilder.mjs'
|
|
97
101
|
export { PcbRouteAnalysisBuilder } from './core/altium/PcbRouteAnalysisBuilder.mjs'
|
|
102
|
+
export { PcbRuleImpactReportBuilder } from './core/altium/PcbRuleImpactReportBuilder.mjs'
|
|
98
103
|
export { PcbRuleParser } from './core/altium/PcbRuleParser.mjs'
|
|
99
104
|
export { PcbSidecarRecordParser } from './core/altium/PcbSidecarRecordParser.mjs'
|
|
100
105
|
export { PcbSpecialStringResolver } from './core/altium/PcbSpecialStringResolver.mjs'
|
|
@@ -113,8 +118,10 @@ export { SchLibStreamExtractor } from './core/altium/SchLibStreamExtractor.mjs'
|
|
|
113
118
|
export { SchematicMultipartOwnerMatcher } from './core/altium/SchematicMultipartOwnerMatcher.mjs'
|
|
114
119
|
export { SchematicCrossSheetConnectorParser } from './core/altium/SchematicCrossSheetConnectorParser.mjs'
|
|
115
120
|
export { SchematicCodeSymbolParser } from './core/altium/SchematicCodeSymbolParser.mjs'
|
|
121
|
+
export { SchematicConnectivityQaBuilder } from './core/altium/SchematicConnectivityQaBuilder.mjs'
|
|
116
122
|
export { SchematicHarnessParser } from './core/altium/SchematicHarnessParser.mjs'
|
|
117
123
|
export { SchematicImplementationParser } from './core/altium/SchematicImplementationParser.mjs'
|
|
124
|
+
export { SchematicImageDiagnosticsBuilder } from './core/altium/SchematicImageDiagnosticsBuilder.mjs'
|
|
118
125
|
export { SchematicOwnershipGraphParser } from './core/altium/SchematicOwnershipGraphParser.mjs'
|
|
119
126
|
export { SchematicProjectParameterResolver } from './core/altium/SchematicProjectParameterResolver.mjs'
|
|
120
127
|
export { SchematicQaReportBuilder } from './core/altium/SchematicQaReportBuilder.mjs'
|
|
@@ -142,6 +142,8 @@
|
|
|
142
142
|
--pcb-via-hole-fill: #0f746c;
|
|
143
143
|
--pcb-footprint-fill: rgba(247, 230, 117, 0.14);
|
|
144
144
|
--pcb-footprint-track-color: rgba(237, 172, 36, 1);
|
|
145
|
+
--pcb-dimension-color: rgba(39, 88, 111, 0.78);
|
|
146
|
+
--pcb-dimension-text: #22343b;
|
|
145
147
|
--pcb-text-knockout-fill: rgba(248, 246, 239, 0.96);
|
|
146
148
|
--pcb-component-top-fill: rgba(244, 219, 198, 0.92);
|
|
147
149
|
--pcb-component-bottom-fill: rgba(15, 116, 108, 0.84);
|
|
@@ -269,6 +271,40 @@
|
|
|
269
271
|
stroke: var(--pcb-footprint-track-color);
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
.pcb-dimensions {
|
|
275
|
+
pointer-events: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.pcb-dimension__measure,
|
|
279
|
+
.pcb-dimension__extension,
|
|
280
|
+
.pcb-dimension__leader,
|
|
281
|
+
.pcb-dimension__arc {
|
|
282
|
+
fill: none;
|
|
283
|
+
stroke: var(--pcb-dimension-color);
|
|
284
|
+
stroke-width: 5;
|
|
285
|
+
stroke-linecap: round;
|
|
286
|
+
stroke-linejoin: round;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.pcb-dimension__extension,
|
|
290
|
+
.pcb-dimension__leader {
|
|
291
|
+
stroke-width: 3;
|
|
292
|
+
stroke-dasharray: 16 10;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.pcb-dimension__center {
|
|
296
|
+
fill: var(--pcb-dimension-color);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.pcb-dimension__label {
|
|
300
|
+
fill: var(--pcb-dimension-text);
|
|
301
|
+
font-family: Arial, sans-serif;
|
|
302
|
+
font-size: 30px;
|
|
303
|
+
font-weight: 700;
|
|
304
|
+
text-anchor: middle;
|
|
305
|
+
dominant-baseline: central;
|
|
306
|
+
}
|
|
307
|
+
|
|
272
308
|
.pcb-component__body {
|
|
273
309
|
fill: var(--pcb-component-top-fill);
|
|
274
310
|
stroke: var(--pcb-component-stroke);
|