altium-toolkit 0.1.1 → 0.1.17
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 +25 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +6 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -12,7 +12,11 @@ import { SchematicDirectiveParser } from './SchematicDirectiveParser.mjs'
|
|
|
12
12
|
import { SchematicPinParser } from './SchematicPinParser.mjs'
|
|
13
13
|
import { SchematicPrimitiveParser } from './SchematicPrimitiveParser.mjs'
|
|
14
14
|
import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
|
|
15
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
15
16
|
import { PcbModelParser } from './PcbModelParser.mjs'
|
|
17
|
+
import { PcbLibModelParser } from './PcbLibModelParser.mjs'
|
|
18
|
+
import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
|
|
19
|
+
import { PrjPcbModelParser } from './PrjPcbModelParser.mjs'
|
|
16
20
|
import { PcbStreamExtractor } from './PcbStreamExtractor.mjs'
|
|
17
21
|
import { SchematicMultipartOwnerMatcher } from './SchematicMultipartOwnerMatcher.mjs'
|
|
18
22
|
import { SchematicSheetStyleResolver } from './SchematicSheetStyleResolver.mjs'
|
|
@@ -21,6 +25,7 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
|
|
|
21
25
|
import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
|
|
22
26
|
import { SchematicImageParser } from './SchematicImageParser.mjs'
|
|
23
27
|
import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
|
|
28
|
+
import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
|
|
24
29
|
const {
|
|
25
30
|
countMatchingKeys,
|
|
26
31
|
getDisplayText,
|
|
@@ -54,7 +59,7 @@ export class AltiumParser {
|
|
|
54
59
|
* Parses a native Altium buffer into a normalized viewer model.
|
|
55
60
|
* @param {string} fileName
|
|
56
61
|
* @param {ArrayBuffer} arrayBuffer
|
|
57
|
-
* @returns {{ kind: 'schematic' | 'pcb', fileType: 'SchDoc' | 'PcbDoc', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
62
|
+
* @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
|
|
58
63
|
*/
|
|
59
64
|
static parseArrayBuffer(fileName, arrayBuffer) {
|
|
60
65
|
const records = AsciiRecordParser.parse(arrayBuffer)
|
|
@@ -71,6 +76,15 @@ export class AltiumParser {
|
|
|
71
76
|
pcbExtraction
|
|
72
77
|
)
|
|
73
78
|
}
|
|
79
|
+
if (fileType === 'PcbLib') {
|
|
80
|
+
return PcbLibModelParser.parse(
|
|
81
|
+
fileName,
|
|
82
|
+
PcbLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
if (fileType === 'PrjPcb') {
|
|
86
|
+
return PrjPcbModelParser.parse(fileName, arrayBuffer)
|
|
87
|
+
}
|
|
74
88
|
throw new Error('Unsupported file type: ' + fileName)
|
|
75
89
|
}
|
|
76
90
|
|
|
@@ -78,12 +92,14 @@ export class AltiumParser {
|
|
|
78
92
|
* Chooses the format based on extension and content.
|
|
79
93
|
* @param {string} fileName
|
|
80
94
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
81
|
-
* @returns {'SchDoc' | 'PcbDoc'}
|
|
95
|
+
* @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
|
|
82
96
|
*/
|
|
83
97
|
static #sniffFileType(fileName, records) {
|
|
84
98
|
const normalized = String(fileName || '').toLowerCase()
|
|
85
99
|
if (normalized.endsWith('.schdoc')) return 'SchDoc'
|
|
86
100
|
if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
|
|
101
|
+
if (normalized.endsWith('.pcblib')) return 'PcbLib'
|
|
102
|
+
if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
|
|
87
103
|
|
|
88
104
|
const hasSchematicHeader = records.some((record) =>
|
|
89
105
|
getField(record.fields, 'HEADER').includes('Schematic')
|
|
@@ -386,7 +402,7 @@ export class AltiumParser {
|
|
|
386
402
|
y,
|
|
387
403
|
libReference,
|
|
388
404
|
designator:
|
|
389
|
-
|
|
405
|
+
SchematicComponentTextResolver.resolveDesignator(
|
|
390
406
|
ownerTexts,
|
|
391
407
|
anchoredTexts,
|
|
392
408
|
{
|
|
@@ -395,7 +411,7 @@ export class AltiumParser {
|
|
|
395
411
|
libReference
|
|
396
412
|
}
|
|
397
413
|
) || 'U?',
|
|
398
|
-
value:
|
|
414
|
+
value: SchematicComponentTextResolver.resolveValue(
|
|
399
415
|
ownerTexts,
|
|
400
416
|
anchoredTexts,
|
|
401
417
|
{ x, y, libReference }
|
|
@@ -474,7 +490,7 @@ export class AltiumParser {
|
|
|
474
490
|
})
|
|
475
491
|
diagnostics.push(...netDiagnostics)
|
|
476
492
|
|
|
477
|
-
return {
|
|
493
|
+
return NormalizedModelSchema.attach({
|
|
478
494
|
kind: 'schematic',
|
|
479
495
|
fileType: 'SchDoc',
|
|
480
496
|
fileName,
|
|
@@ -511,7 +527,7 @@ export class AltiumParser {
|
|
|
511
527
|
nets
|
|
512
528
|
},
|
|
513
529
|
bom
|
|
514
|
-
}
|
|
530
|
+
})
|
|
515
531
|
}
|
|
516
532
|
|
|
517
533
|
/**
|
|
@@ -529,21 +545,6 @@ export class AltiumParser {
|
|
|
529
545
|
return match ? getDisplayText(match.fields) : ''
|
|
530
546
|
}
|
|
531
547
|
|
|
532
|
-
/**
|
|
533
|
-
* Finds a related text value by name.
|
|
534
|
-
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
535
|
-
* @param {string} logicalName
|
|
536
|
-
* @returns {string}
|
|
537
|
-
*/
|
|
538
|
-
static #findRelatedText(records, logicalName) {
|
|
539
|
-
const match = records.find(
|
|
540
|
-
(record) =>
|
|
541
|
-
getField(record.fields, 'Name').toLowerCase() ===
|
|
542
|
-
logicalName.toLowerCase()
|
|
543
|
-
)
|
|
544
|
-
return match ? getDisplayText(match.fields) : ''
|
|
545
|
-
}
|
|
546
|
-
|
|
547
548
|
/**
|
|
548
549
|
* Collects owners whose active symbol primitives already exist without an
|
|
549
550
|
* explicit display-mode selector.
|
|
@@ -636,310 +637,6 @@ export class AltiumParser {
|
|
|
636
637
|
)
|
|
637
638
|
}
|
|
638
639
|
|
|
639
|
-
/**
|
|
640
|
-
* Resolves a component designator from owner-linked text or nearby visible
|
|
641
|
-
* schematic labels when the owner link is missing.
|
|
642
|
-
* @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
|
|
643
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
644
|
-
* @param {{ x: number, y: number, libReference: string }} component
|
|
645
|
-
* @returns {string}
|
|
646
|
-
*/
|
|
647
|
-
static #resolveComponentDesignator(ownerTexts, texts, component) {
|
|
648
|
-
const ownerDesignator = AltiumParser.#findRelatedText(
|
|
649
|
-
ownerTexts,
|
|
650
|
-
'Designator'
|
|
651
|
-
)
|
|
652
|
-
if (AltiumParser.#isResolvedComponentText(ownerDesignator)) {
|
|
653
|
-
return ownerDesignator
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
return AltiumParser.#findNearbyComponentDesignator(texts, component)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Resolves a component value from owner-linked text or nearby visible
|
|
661
|
-
* schematic labels when the owner link still contains template placeholders.
|
|
662
|
-
* @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
|
|
663
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
664
|
-
* @param {{ x: number, y: number, libReference: string }} component
|
|
665
|
-
* @returns {string}
|
|
666
|
-
*/
|
|
667
|
-
static #resolveComponentValue(ownerTexts, texts, component) {
|
|
668
|
-
const ownerValue =
|
|
669
|
-
AltiumParser.#findRelatedText(ownerTexts, 'Comment') ||
|
|
670
|
-
AltiumParser.#findRelatedText(ownerTexts, 'VALUE')
|
|
671
|
-
if (AltiumParser.#isResolvedComponentText(ownerValue)) {
|
|
672
|
-
return ownerValue
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return (
|
|
676
|
-
AltiumParser.#findNearbyComponentText(
|
|
677
|
-
texts,
|
|
678
|
-
component,
|
|
679
|
-
['comment', 'value'],
|
|
680
|
-
'',
|
|
681
|
-
AltiumParser.#inferComponentValueHint(component.libReference)
|
|
682
|
-
) || ownerValue
|
|
683
|
-
)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Finds the closest nearby designator text for one component.
|
|
688
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
689
|
-
* @param {{ x: number, y: number, libReference: string }} component
|
|
690
|
-
* @returns {string}
|
|
691
|
-
*/
|
|
692
|
-
static #findNearbyComponentDesignator(texts, component) {
|
|
693
|
-
const expectedPrefix = AltiumParser.#inferComponentDesignatorPrefix(
|
|
694
|
-
component.libReference
|
|
695
|
-
)
|
|
696
|
-
const expectedValueHint = AltiumParser.#inferComponentValueHint(
|
|
697
|
-
component.libReference
|
|
698
|
-
)
|
|
699
|
-
const candidates = AltiumParser.#collectNearbyComponentTextCandidates(
|
|
700
|
-
texts,
|
|
701
|
-
component,
|
|
702
|
-
['designator']
|
|
703
|
-
)
|
|
704
|
-
const scopedCandidates = expectedPrefix
|
|
705
|
-
? candidates.filter((candidate) =>
|
|
706
|
-
candidate.text
|
|
707
|
-
.toUpperCase()
|
|
708
|
-
.startsWith(expectedPrefix.toUpperCase())
|
|
709
|
-
)
|
|
710
|
-
: candidates
|
|
711
|
-
const usableCandidates = scopedCandidates.length
|
|
712
|
-
? scopedCandidates
|
|
713
|
-
: candidates
|
|
714
|
-
const rankedCandidates = usableCandidates
|
|
715
|
-
.map((candidate) => ({
|
|
716
|
-
...candidate,
|
|
717
|
-
score:
|
|
718
|
-
candidate.distance +
|
|
719
|
-
AltiumParser.#scoreAssociatedValueMismatch(
|
|
720
|
-
texts,
|
|
721
|
-
candidate,
|
|
722
|
-
expectedValueHint
|
|
723
|
-
)
|
|
724
|
-
}))
|
|
725
|
-
.sort((left, right) => left.score - right.score)
|
|
726
|
-
|
|
727
|
-
return rankedCandidates[0]?.text || ''
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Finds the closest nearby visible text for one component.
|
|
732
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
733
|
-
* @param {{ x: number, y: number }} component
|
|
734
|
-
* @param {string[]} logicalNames
|
|
735
|
-
* @param {string} expectedPrefix
|
|
736
|
-
* @param {string} expectedTextHint
|
|
737
|
-
* @returns {string}
|
|
738
|
-
*/
|
|
739
|
-
static #findNearbyComponentText(
|
|
740
|
-
texts,
|
|
741
|
-
component,
|
|
742
|
-
logicalNames,
|
|
743
|
-
expectedPrefix = '',
|
|
744
|
-
expectedTextHint = ''
|
|
745
|
-
) {
|
|
746
|
-
const candidates = AltiumParser.#collectNearbyComponentTextCandidates(
|
|
747
|
-
texts,
|
|
748
|
-
component,
|
|
749
|
-
logicalNames
|
|
750
|
-
)
|
|
751
|
-
const prefixedCandidates = expectedPrefix
|
|
752
|
-
? candidates.filter((candidate) =>
|
|
753
|
-
candidate.text
|
|
754
|
-
.toUpperCase()
|
|
755
|
-
.startsWith(expectedPrefix.toUpperCase())
|
|
756
|
-
)
|
|
757
|
-
: candidates
|
|
758
|
-
const scopedCandidates = prefixedCandidates.length
|
|
759
|
-
? prefixedCandidates
|
|
760
|
-
: candidates
|
|
761
|
-
const hintedCandidates = expectedTextHint
|
|
762
|
-
? scopedCandidates.filter((candidate) =>
|
|
763
|
-
AltiumParser.#normalizeTextMatch(candidate.text).includes(
|
|
764
|
-
AltiumParser.#normalizeTextMatch(expectedTextHint)
|
|
765
|
-
)
|
|
766
|
-
)
|
|
767
|
-
: scopedCandidates
|
|
768
|
-
const usableCandidates = hintedCandidates.length
|
|
769
|
-
? hintedCandidates
|
|
770
|
-
: scopedCandidates
|
|
771
|
-
|
|
772
|
-
return usableCandidates.sort(
|
|
773
|
-
(left, right) => left.distance - right.distance
|
|
774
|
-
)[0]?.text
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Collects nearby visible schematic text candidates around one component.
|
|
779
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
780
|
-
* @param {{ x: number, y: number }} component
|
|
781
|
-
* @param {string[]} logicalNames
|
|
782
|
-
* @returns {{ x: number, y: number, text: string, distance: number }[]}
|
|
783
|
-
*/
|
|
784
|
-
static #collectNearbyComponentTextCandidates(
|
|
785
|
-
texts,
|
|
786
|
-
component,
|
|
787
|
-
logicalNames
|
|
788
|
-
) {
|
|
789
|
-
const allowedNames = new Set(
|
|
790
|
-
logicalNames.map((name) => name.toLowerCase())
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
return texts
|
|
794
|
-
.filter((text) =>
|
|
795
|
-
allowedNames.has(
|
|
796
|
-
String(text.name || '')
|
|
797
|
-
.trim()
|
|
798
|
-
.toLowerCase()
|
|
799
|
-
)
|
|
800
|
-
)
|
|
801
|
-
.map((text) => ({
|
|
802
|
-
x: text.x,
|
|
803
|
-
y: text.y,
|
|
804
|
-
text: text.text,
|
|
805
|
-
distance:
|
|
806
|
-
Math.abs(text.x - component.x) +
|
|
807
|
-
Math.abs(text.y - component.y)
|
|
808
|
-
}))
|
|
809
|
-
.filter(
|
|
810
|
-
(text) =>
|
|
811
|
-
Math.abs(text.x - component.x) <= 80 &&
|
|
812
|
-
Math.abs(text.y - component.y) <= 80
|
|
813
|
-
)
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Penalizes a designator candidate when its nearby value text does not
|
|
818
|
-
* match the library-derived value hint.
|
|
819
|
-
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
820
|
-
* @param {{ x: number, y: number }} candidate
|
|
821
|
-
* @param {string} expectedValueHint
|
|
822
|
-
* @returns {number}
|
|
823
|
-
*/
|
|
824
|
-
static #scoreAssociatedValueMismatch(texts, candidate, expectedValueHint) {
|
|
825
|
-
if (!expectedValueHint) {
|
|
826
|
-
return 0
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const associatedValue = AltiumParser.#findNearbyComponentText(
|
|
830
|
-
texts,
|
|
831
|
-
candidate,
|
|
832
|
-
['comment', 'value']
|
|
833
|
-
)
|
|
834
|
-
if (!associatedValue) {
|
|
835
|
-
return 0
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
return AltiumParser.#normalizeTextMatch(associatedValue).includes(
|
|
839
|
-
AltiumParser.#normalizeTextMatch(expectedValueHint)
|
|
840
|
-
)
|
|
841
|
-
? -30
|
|
842
|
-
: 30
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* Returns true when a recovered owner-linked text is usable as a component
|
|
847
|
-
* display value.
|
|
848
|
-
* @param {string} value
|
|
849
|
-
* @returns {boolean}
|
|
850
|
-
*/
|
|
851
|
-
static #isResolvedComponentText(value) {
|
|
852
|
-
const normalized = String(value || '').trim()
|
|
853
|
-
|
|
854
|
-
return Boolean(
|
|
855
|
-
normalized && normalized !== '*' && !normalized.startsWith('=')
|
|
856
|
-
)
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Infers the visible designator prefix from a library reference.
|
|
861
|
-
* @param {string} libReference
|
|
862
|
-
* @returns {string}
|
|
863
|
-
*/
|
|
864
|
-
static #inferComponentDesignatorPrefix(libReference) {
|
|
865
|
-
const normalized = String(libReference || '')
|
|
866
|
-
.trim()
|
|
867
|
-
.toUpperCase()
|
|
868
|
-
|
|
869
|
-
if (normalized.startsWith('RES/')) return 'R'
|
|
870
|
-
if (normalized.startsWith('CAP/')) return 'C'
|
|
871
|
-
if (normalized.startsWith('DIODE/')) return 'D'
|
|
872
|
-
if (normalized.startsWith('CON/')) return 'J'
|
|
873
|
-
if (normalized.startsWith('IC/')) return 'U'
|
|
874
|
-
|
|
875
|
-
return ''
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Infers the visible value label from a library reference.
|
|
880
|
-
* @param {string} libReference
|
|
881
|
-
* @returns {string}
|
|
882
|
-
*/
|
|
883
|
-
static #inferComponentValueHint(libReference) {
|
|
884
|
-
const segments = String(libReference || '')
|
|
885
|
-
.split('/')
|
|
886
|
-
.map((segment) => segment.trim())
|
|
887
|
-
.filter(Boolean)
|
|
888
|
-
|
|
889
|
-
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
890
|
-
const segment = segments[index]
|
|
891
|
-
|
|
892
|
-
if (
|
|
893
|
-
AltiumParser.#isPackageLikeComponentSegment(segment) ||
|
|
894
|
-
/\s/.test(segment)
|
|
895
|
-
) {
|
|
896
|
-
continue
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (
|
|
900
|
-
/^(?:\d+(?:\.\d+)?(?:R|K|M|UF|NF|PF)|1N[A-Z0-9-]+)$/i.test(
|
|
901
|
-
segment
|
|
902
|
-
)
|
|
903
|
-
) {
|
|
904
|
-
return segment
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
/[A-Z]/i.test(segment) &&
|
|
909
|
-
/\d/.test(segment) &&
|
|
910
|
-
segment.length >= 6
|
|
911
|
-
) {
|
|
912
|
-
return segment
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return ''
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Returns true when one library segment behaves like a package or rating
|
|
921
|
-
* rather than a user-facing value.
|
|
922
|
-
* @param {string} segment
|
|
923
|
-
* @returns {boolean}
|
|
924
|
-
*/
|
|
925
|
-
static #isPackageLikeComponentSegment(segment) {
|
|
926
|
-
return /^(?:CE|\d{4}|SC\d+|SOD-\d+|\d+(?:\.\d+)?V|\d+(?:\.\d+)?[%%])$/i.test(
|
|
927
|
-
String(segment || '').trim()
|
|
928
|
-
)
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Normalizes a text fragment for proximity matching.
|
|
933
|
-
* @param {string} value
|
|
934
|
-
* @returns {string}
|
|
935
|
-
*/
|
|
936
|
-
static #normalizeTextMatch(value) {
|
|
937
|
-
return String(value || '')
|
|
938
|
-
.toUpperCase()
|
|
939
|
-
.replaceAll(/\s+/g, '')
|
|
940
|
-
.replaceAll('%', '%')
|
|
941
|
-
}
|
|
942
|
-
|
|
943
640
|
/**
|
|
944
641
|
* Groups designators into BOM rows.
|
|
945
642
|
* @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines the current normalized model contract emitted by parser roots.
|
|
7
|
+
*/
|
|
8
|
+
export class NormalizedModelSchema {
|
|
9
|
+
static CURRENT_SCHEMA_ID = 'urn:altium-toolkit:normalized-model:a1'
|
|
10
|
+
|
|
11
|
+
static CURRENT_SCHEMA_VERSION = 'a1'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adds the current normalized model schema id to a parser root object.
|
|
15
|
+
* @template {Record<string, unknown>} T
|
|
16
|
+
* @param {T} model
|
|
17
|
+
* @returns {T & { schema: string }}
|
|
18
|
+
*/
|
|
19
|
+
static attach(model) {
|
|
20
|
+
const normalizedModel = {
|
|
21
|
+
schema: NormalizedModelSchema.CURRENT_SCHEMA_ID,
|
|
22
|
+
...model
|
|
23
|
+
}
|
|
24
|
+
normalizedModel.schema = NormalizedModelSchema.CURRENT_SCHEMA_ID
|
|
25
|
+
|
|
26
|
+
return normalizedModel
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbPrimitiveRecordSlicer } from './PcbPrimitiveRecordSlicer.mjs'
|
|
6
|
+
import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decodes Altium arc primitive streams.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbArcPrimitiveParser {
|
|
12
|
+
static #ARC_OBJECT_ID = 1
|
|
13
|
+
|
|
14
|
+
static #ARC_RECORD_BYTE_LENGTH = 60
|
|
15
|
+
|
|
16
|
+
static #ARC_PAYLOAD_MIN_BYTE_LENGTH = 45
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decodes one arc stream.
|
|
20
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
21
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
22
|
+
* @returns {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }[]}
|
|
23
|
+
*/
|
|
24
|
+
static parseArcStream(headerBytes, dataBytes) {
|
|
25
|
+
return PcbArcPrimitiveParser.#sliceArcRecords(
|
|
26
|
+
headerBytes,
|
|
27
|
+
dataBytes
|
|
28
|
+
).map((view) => PcbArcPrimitiveParser.#parseArcRecord(view))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decodes one arc record view into a normalized primitive.
|
|
33
|
+
* @param {DataView} view
|
|
34
|
+
* @returns {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }}
|
|
35
|
+
*/
|
|
36
|
+
static #parseArcRecord(view) {
|
|
37
|
+
const layerId = view.getUint8(0)
|
|
38
|
+
const ownershipIndexes =
|
|
39
|
+
PcbPrimitiveOwnershipIndexParser.readOwnershipIndexes(view, {
|
|
40
|
+
component: 7,
|
|
41
|
+
net: 3,
|
|
42
|
+
polygon: 5
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
x: PcbArcPrimitiveParser.#readMil(view, 13),
|
|
47
|
+
y: PcbArcPrimitiveParser.#readMil(view, 17),
|
|
48
|
+
radius: PcbArcPrimitiveParser.#readMil(view, 21),
|
|
49
|
+
startAngle: view.getFloat64(25, true),
|
|
50
|
+
endAngle: view.getFloat64(33, true),
|
|
51
|
+
width: PcbArcPrimitiveParser.#readMil(view, 41),
|
|
52
|
+
...ownershipIndexes,
|
|
53
|
+
layerCode: layerId,
|
|
54
|
+
layerId
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Splits an arc stream into record views, preserving variable payload
|
|
60
|
+
* lengths when arcs are stored with object-id and payload-length prefixes.
|
|
61
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
62
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
63
|
+
* @returns {DataView[]}
|
|
64
|
+
*/
|
|
65
|
+
static #sliceArcRecords(headerBytes, dataBytes) {
|
|
66
|
+
return PcbPrimitiveRecordSlicer.slicePrimitiveRecords({
|
|
67
|
+
headerBytes,
|
|
68
|
+
dataBytes,
|
|
69
|
+
objectId: PcbArcPrimitiveParser.#ARC_OBJECT_ID,
|
|
70
|
+
fixedRecordByteLength:
|
|
71
|
+
PcbArcPrimitiveParser.#ARC_RECORD_BYTE_LENGTH,
|
|
72
|
+
minimumPayloadByteLength:
|
|
73
|
+
PcbArcPrimitiveParser.#ARC_PAYLOAD_MIN_BYTE_LENGTH,
|
|
74
|
+
lengthPrefixedView: 'payload'
|
|
75
|
+
}).map((record) => record.view)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reads one standard fixed-point mil value.
|
|
80
|
+
* @param {DataView} view
|
|
81
|
+
* @param {number} offset
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
static #readMil(view, offset) {
|
|
85
|
+
return view.getInt32(offset, true) / 10000
|
|
86
|
+
}
|
|
87
|
+
}
|