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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { PcbGeometryFlipper } from './PcbGeometryFlipper.mjs'
|
|
5
6
|
import { PcbOutlineRasterizer } from './PcbOutlineRasterizer.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -92,81 +93,11 @@ export class PcbOutlineRecovery {
|
|
|
92
93
|
/**
|
|
93
94
|
* Mirrors one normalized PCB model vertically so the SVG matches the
|
|
94
95
|
* authored top-view orientation.
|
|
95
|
-
* @param {{ boardOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, polygons?: { layer?: string, segments: Array<Record<string, number | string>> }[], fills?: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks?: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], vias?: { x: number, y: number, diameter: number, holeDiameter: number }[], pads?: { x: number, y: number, rotation?: number, holeRotation?: number | null }[], components?: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] }} pcb
|
|
96
|
-
* @returns {{ boardOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, polygons: { layer?: string, segments: Array<Record<string, number | string>> }[], fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], vias: { x: number, y: number, diameter: number, holeDiameter: number }[], pads: { x: number, y: number, rotation?: number, holeRotation?: number | null }[], components: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] }}
|
|
96
|
+
* @param {{ boardOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, polygons?: { layer?: string, segments: Array<Record<string, number | string>> }[], fills?: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks?: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], vias?: { x: number, y: number, diameter: number, holeDiameter: number }[], pads?: { x: number, y: number, rotation?: number, holeRotation?: number | null }[], texts?: { x: number, y: number, rotation?: number }[], components?: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] }} pcb
|
|
97
|
+
* @returns {{ boardOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, polygons: { layer?: string, segments: Array<Record<string, number | string>> }[], fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], vias: { x: number, y: number, diameter: number, holeDiameter: number }[], pads: { x: number, y: number, rotation?: number, holeRotation?: number | null }[], texts: { x: number, y: number, rotation?: number }[], components: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] }}
|
|
97
98
|
*/
|
|
98
99
|
static flipGeometryVertically(pcb) {
|
|
99
|
-
|
|
100
|
-
const maxY =
|
|
101
|
-
Number(outline?.minY || 0) + Number(outline?.heightMil || 0)
|
|
102
|
-
const mirrorY = (value) =>
|
|
103
|
-
Number(outline?.minY || 0) + maxY - Number(value || 0)
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
...pcb,
|
|
107
|
-
boardOutline: {
|
|
108
|
-
...outline,
|
|
109
|
-
segments: (outline?.segments || []).map((segment) =>
|
|
110
|
-
PcbOutlineRecovery.#flipSegment(segment, mirrorY)
|
|
111
|
-
)
|
|
112
|
-
},
|
|
113
|
-
polygons: (pcb?.polygons || []).map((polygon) => ({
|
|
114
|
-
...polygon,
|
|
115
|
-
segments: (polygon.segments || []).map((segment) =>
|
|
116
|
-
PcbOutlineRecovery.#flipSegment(segment, mirrorY)
|
|
117
|
-
)
|
|
118
|
-
})),
|
|
119
|
-
fills: (pcb?.fills || []).map((fill) => {
|
|
120
|
-
const y1 = mirrorY(fill.y1)
|
|
121
|
-
const y2 = mirrorY(fill.y2)
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
...fill,
|
|
125
|
-
y1: Math.min(y1, y2),
|
|
126
|
-
y2: Math.max(y1, y2)
|
|
127
|
-
}
|
|
128
|
-
}),
|
|
129
|
-
tracks: (pcb?.tracks || []).map((track) => ({
|
|
130
|
-
...track,
|
|
131
|
-
y1: mirrorY(track.y1),
|
|
132
|
-
y2: mirrorY(track.y2)
|
|
133
|
-
})),
|
|
134
|
-
arcs: (pcb?.arcs || []).map((arc) => ({
|
|
135
|
-
...arc,
|
|
136
|
-
y: mirrorY(arc.y),
|
|
137
|
-
startAngle: PcbOutlineRecovery.#normalizeAngle(
|
|
138
|
-
360 - Number(arc.startAngle || 0)
|
|
139
|
-
),
|
|
140
|
-
endAngle: PcbOutlineRecovery.#normalizeAngle(
|
|
141
|
-
360 - Number(arc.endAngle || 0)
|
|
142
|
-
)
|
|
143
|
-
})),
|
|
144
|
-
vias: (pcb?.vias || []).map((via) => ({
|
|
145
|
-
...via,
|
|
146
|
-
y: mirrorY(via.y)
|
|
147
|
-
})),
|
|
148
|
-
pads: (pcb?.pads || []).map((pad) => ({
|
|
149
|
-
...pad,
|
|
150
|
-
y: mirrorY(pad.y),
|
|
151
|
-
rotation: PcbOutlineRecovery.#normalizeAngle(
|
|
152
|
-
360 - Number(pad.rotation || 0)
|
|
153
|
-
),
|
|
154
|
-
holeRotation:
|
|
155
|
-
pad?.holeRotation === null ||
|
|
156
|
-
pad?.holeRotation === undefined
|
|
157
|
-
? (pad?.holeRotation ?? null)
|
|
158
|
-
: PcbOutlineRecovery.#normalizeAngle(
|
|
159
|
-
360 - Number(pad.holeRotation || 0)
|
|
160
|
-
)
|
|
161
|
-
})),
|
|
162
|
-
components: (pcb?.components || []).map((component) => ({
|
|
163
|
-
...component,
|
|
164
|
-
y: mirrorY(component.y),
|
|
165
|
-
rotation: PcbOutlineRecovery.#normalizeAngle(
|
|
166
|
-
360 - Number(component.rotation || 0)
|
|
167
|
-
)
|
|
168
|
-
}))
|
|
169
|
-
}
|
|
100
|
+
return PcbGeometryFlipper.flipGeometryVertically(pcb)
|
|
170
101
|
}
|
|
171
102
|
|
|
172
103
|
/**
|
|
@@ -915,43 +846,4 @@ export class PcbOutlineRecovery {
|
|
|
915
846
|
|
|
916
847
|
return delta
|
|
917
848
|
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Mirrors one outline or polygon segment across the board Y axis.
|
|
921
|
-
* @param {Record<string, number | string>} segment
|
|
922
|
-
* @param {(value: number) => number} mirrorY
|
|
923
|
-
* @returns {Record<string, number | string>}
|
|
924
|
-
*/
|
|
925
|
-
static #flipSegment(segment, mirrorY) {
|
|
926
|
-
if (segment.type !== 'arc') {
|
|
927
|
-
return {
|
|
928
|
-
...segment,
|
|
929
|
-
y1: mirrorY(Number(segment.y1 || 0)),
|
|
930
|
-
y2: mirrorY(Number(segment.y2 || 0))
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const startAngle = Number(segment.startAngle || 0)
|
|
935
|
-
const endAngle = Number(segment.endAngle || 0)
|
|
936
|
-
|
|
937
|
-
return {
|
|
938
|
-
...segment,
|
|
939
|
-
y1: mirrorY(Number(segment.y1 || 0)),
|
|
940
|
-
y2: mirrorY(Number(segment.y2 || 0)),
|
|
941
|
-
cy: mirrorY(Number(segment.cy || 0)),
|
|
942
|
-
startAngle: PcbOutlineRecovery.#normalizeAngle(360 - startAngle),
|
|
943
|
-
endAngle: PcbOutlineRecovery.#normalizeAngle(360 - endAngle)
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* Normalizes one circular angle into the [0, 360) range.
|
|
949
|
-
* @param {number} angle
|
|
950
|
-
* @returns {number}
|
|
951
|
-
*/
|
|
952
|
-
static #normalizeAngle(angle) {
|
|
953
|
-
const normalized = Number(angle || 0) % 360
|
|
954
|
-
|
|
955
|
-
return normalized < 0 ? normalized + 360 : normalized
|
|
956
|
-
}
|
|
957
849
|
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbLayerIdCodec } from './PcbLayerIdCodec.mjs'
|
|
6
|
+
import { PcbPadShapeCodec } from './PcbPadShapeCodec.mjs'
|
|
7
|
+
import { PcbPadStackParser } from './PcbPadStackParser.mjs'
|
|
8
|
+
import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decodes Altium pad primitive streams.
|
|
12
|
+
*/
|
|
13
|
+
export class PcbPadPrimitiveParser {
|
|
14
|
+
static #PAD_OBJECT_ID = 2
|
|
15
|
+
|
|
16
|
+
static #PAD_SUBRECORD_COUNT = 6
|
|
17
|
+
|
|
18
|
+
static #PAD_MAIN_SUBRECORD_INDEX = 4
|
|
19
|
+
|
|
20
|
+
static #PAD_EXTENSION_SUBRECORD_INDEX = 5
|
|
21
|
+
|
|
22
|
+
static #PAD_MAIN_RECORD_MIN_BYTE_LENGTH = 61
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decodes one variable-length pad stream.
|
|
26
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
27
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
28
|
+
* @returns {{ x: number, y: number, sizeTopX: number, sizeTopY: number, sizeMidX: number, sizeMidY: number, sizeBottomX: number, sizeBottomY: number, holeDiameter: number, shapeTop: number, shapeMid: number, shapeBottom: number, shapeTopName: string | null, shapeMidName: string | null, shapeBottomName: string | null, padShapeNames: { top: string | null, middle: string | null, bottom: string | null }, rotation: number, isPlated: boolean, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number | null, layerId: number | null, legacyLayerId: number | null, layerV7SaveId: number | null, [key: string]: unknown }[]}
|
|
29
|
+
*/
|
|
30
|
+
static parsePadStream(headerBytes, dataBytes) {
|
|
31
|
+
const count = PcbPadPrimitiveParser.#readRecordCount(headerBytes)
|
|
32
|
+
const normalizedData = PcbPadPrimitiveParser.#toUint8Array(dataBytes)
|
|
33
|
+
|
|
34
|
+
if (!count) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let offset = 0
|
|
39
|
+
const pads = []
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < count; index += 1) {
|
|
42
|
+
const record = PcbPadPrimitiveParser.#readPadRecordAt(
|
|
43
|
+
normalizedData,
|
|
44
|
+
offset
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (!record) {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pad = PcbPadPrimitiveParser.#parsePadSubrecords(
|
|
52
|
+
record.subrecords
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (!pad) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pads.push(pad)
|
|
60
|
+
offset = record.nextOffset
|
|
61
|
+
|
|
62
|
+
if (index < count - 1) {
|
|
63
|
+
const nextOffset =
|
|
64
|
+
PcbPadPrimitiveParser.#findNextPadRecordOffset(
|
|
65
|
+
normalizedData,
|
|
66
|
+
offset,
|
|
67
|
+
count - index - 1
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (nextOffset === null) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
offset = nextOffset
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return pads
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reads one pad record and its known subrecords from a stream offset.
|
|
83
|
+
* @param {Uint8Array} bytes
|
|
84
|
+
* @param {number} offset
|
|
85
|
+
* @returns {{ subrecords: DataView[], nextOffset: number } | null}
|
|
86
|
+
*/
|
|
87
|
+
static #readPadRecordAt(bytes, offset) {
|
|
88
|
+
if (
|
|
89
|
+
offset + 1 > bytes.byteLength ||
|
|
90
|
+
bytes[offset] !== PcbPadPrimitiveParser.#PAD_OBJECT_ID
|
|
91
|
+
) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let cursor = offset + 1
|
|
96
|
+
const subrecords = []
|
|
97
|
+
|
|
98
|
+
for (
|
|
99
|
+
let subrecordIndex = 0;
|
|
100
|
+
subrecordIndex < PcbPadPrimitiveParser.#PAD_SUBRECORD_COUNT;
|
|
101
|
+
subrecordIndex += 1
|
|
102
|
+
) {
|
|
103
|
+
const subrecord = PcbPadPrimitiveParser.#readSubrecordAt(
|
|
104
|
+
bytes,
|
|
105
|
+
cursor
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (!subrecord) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
subrecords.push(subrecord.view)
|
|
113
|
+
cursor = subrecord.nextOffset
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { subrecords, nextOffset: cursor }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reads one length-prefixed pad subrecord.
|
|
121
|
+
* @param {Uint8Array} bytes
|
|
122
|
+
* @param {number} offset
|
|
123
|
+
* @returns {{ view: DataView, nextOffset: number } | null}
|
|
124
|
+
*/
|
|
125
|
+
static #readSubrecordAt(bytes, offset) {
|
|
126
|
+
if (offset + 4 > bytes.byteLength) {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const subrecordLength = PcbPadPrimitiveParser.#readUint32FromBytes(
|
|
131
|
+
bytes,
|
|
132
|
+
offset
|
|
133
|
+
)
|
|
134
|
+
const payloadOffset = offset + 4
|
|
135
|
+
const nextOffset = payloadOffset + subrecordLength
|
|
136
|
+
|
|
137
|
+
if (nextOffset > bytes.byteLength) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
view: new DataView(
|
|
143
|
+
bytes.buffer,
|
|
144
|
+
bytes.byteOffset + payloadOffset,
|
|
145
|
+
subrecordLength
|
|
146
|
+
),
|
|
147
|
+
nextOffset
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Finds the next pad record boundary after optional unknown subrecords.
|
|
153
|
+
* @param {Uint8Array} bytes
|
|
154
|
+
* @param {number} offset
|
|
155
|
+
* @param {number} remainingCount
|
|
156
|
+
* @returns {number | null}
|
|
157
|
+
*/
|
|
158
|
+
static #findNextPadRecordOffset(bytes, offset, remainingCount) {
|
|
159
|
+
let cursor = offset
|
|
160
|
+
|
|
161
|
+
while (cursor < bytes.byteLength) {
|
|
162
|
+
if (
|
|
163
|
+
PcbPadPrimitiveParser.#canReadPadRecordSequence(
|
|
164
|
+
bytes,
|
|
165
|
+
cursor,
|
|
166
|
+
remainingCount
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
return cursor
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const unknownSubrecord = PcbPadPrimitiveParser.#readSubrecordAt(
|
|
173
|
+
bytes,
|
|
174
|
+
cursor
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (!unknownSubrecord) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cursor = unknownSubrecord.nextOffset
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Checks whether the remaining pad records can be read from an offset.
|
|
189
|
+
* @param {Uint8Array} bytes
|
|
190
|
+
* @param {number} offset
|
|
191
|
+
* @param {number} remainingCount
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
static #canReadPadRecordSequence(bytes, offset, remainingCount) {
|
|
195
|
+
const record = PcbPadPrimitiveParser.#readPadRecordAt(bytes, offset)
|
|
196
|
+
|
|
197
|
+
if (!record) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (remainingCount <= 1) {
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
PcbPadPrimitiveParser.#findNextPadRecordOffset(
|
|
207
|
+
bytes,
|
|
208
|
+
record.nextOffset,
|
|
209
|
+
remainingCount - 1
|
|
210
|
+
) !== null
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Decodes one pad payload from its subrecords.
|
|
216
|
+
* @param {DataView[]} subrecords
|
|
217
|
+
* @returns {{ x: number, y: number, sizeTopX: number, sizeTopY: number, sizeMidX: number, sizeMidY: number, sizeBottomX: number, sizeBottomY: number, holeDiameter: number, shapeTop: number, shapeMid: number, shapeBottom: number, shapeTopName: string | null, shapeMidName: string | null, shapeBottomName: string | null, padShapeNames: { top: string | null, middle: string | null, bottom: string | null }, rotation: number, isPlated: boolean, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number | null, layerId: number | null, legacyLayerId: number | null, layerV7SaveId: number | null, [key: string]: unknown } | null}
|
|
218
|
+
*/
|
|
219
|
+
static #parsePadSubrecords(subrecords) {
|
|
220
|
+
const mainRecord =
|
|
221
|
+
subrecords[PcbPadPrimitiveParser.#PAD_MAIN_SUBRECORD_INDEX]
|
|
222
|
+
const extensionRecord =
|
|
223
|
+
subrecords[PcbPadPrimitiveParser.#PAD_EXTENSION_SUBRECORD_INDEX]
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
!mainRecord ||
|
|
227
|
+
mainRecord.byteLength <
|
|
228
|
+
PcbPadPrimitiveParser.#PAD_MAIN_RECORD_MIN_BYTE_LENGTH
|
|
229
|
+
) {
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const layerState = PcbPadPrimitiveParser.#parsePadLayerState(mainRecord)
|
|
234
|
+
const ownershipIndexes =
|
|
235
|
+
PcbPrimitiveOwnershipIndexParser.readOwnershipIndexes(mainRecord, {
|
|
236
|
+
component: 7,
|
|
237
|
+
net: 3,
|
|
238
|
+
polygon: 5
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const pad = {
|
|
242
|
+
x: PcbPadPrimitiveParser.#readMil(mainRecord, 13),
|
|
243
|
+
y: PcbPadPrimitiveParser.#readMil(mainRecord, 17),
|
|
244
|
+
sizeTopX: PcbPadPrimitiveParser.#readMil(mainRecord, 21),
|
|
245
|
+
sizeTopY: PcbPadPrimitiveParser.#readMil(mainRecord, 25),
|
|
246
|
+
sizeMidX: PcbPadPrimitiveParser.#readMil(mainRecord, 29),
|
|
247
|
+
sizeMidY: PcbPadPrimitiveParser.#readMil(mainRecord, 33),
|
|
248
|
+
sizeBottomX: PcbPadPrimitiveParser.#readMil(mainRecord, 37),
|
|
249
|
+
sizeBottomY: PcbPadPrimitiveParser.#readMil(mainRecord, 41),
|
|
250
|
+
holeDiameter: PcbPadPrimitiveParser.#readMil(mainRecord, 45),
|
|
251
|
+
shapeTop: mainRecord.getUint8(49),
|
|
252
|
+
shapeMid: mainRecord.getUint8(50),
|
|
253
|
+
shapeBottom: mainRecord.getUint8(51),
|
|
254
|
+
rotation: mainRecord.getFloat64(52, true),
|
|
255
|
+
isPlated: mainRecord.getUint8(60) !== 0,
|
|
256
|
+
...ownershipIndexes,
|
|
257
|
+
layerCode: layerState.layerId,
|
|
258
|
+
layerId: layerState.layerId,
|
|
259
|
+
legacyLayerId: layerState.legacyLayerId,
|
|
260
|
+
layerV7SaveId: layerState.layerV7SaveId
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
...pad,
|
|
265
|
+
...PcbPadShapeCodec.describePadShapes(pad),
|
|
266
|
+
...PcbPadStackParser.parse(mainRecord, extensionRecord, pad)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Decodes the visible and hidden saved-layer state from one pad main record.
|
|
272
|
+
* @param {DataView} mainRecord
|
|
273
|
+
* @returns {{ layerId: number | null, legacyLayerId: number | null, layerV7SaveId: number | null }}
|
|
274
|
+
*/
|
|
275
|
+
static #parsePadLayerState(mainRecord) {
|
|
276
|
+
const legacyLayerId = mainRecord.getUint8(0) || null
|
|
277
|
+
const layerV7SaveId =
|
|
278
|
+
mainRecord.byteLength >= 118
|
|
279
|
+
? mainRecord.getUint32(114, true) || null
|
|
280
|
+
: null
|
|
281
|
+
const decodedLayerId =
|
|
282
|
+
PcbLayerIdCodec.legacyLayerIdFromV7SaveId(layerV7SaveId)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
layerId: decodedLayerId || legacyLayerId,
|
|
286
|
+
legacyLayerId,
|
|
287
|
+
layerV7SaveId
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Reads one standard fixed-point mil value.
|
|
293
|
+
* @param {DataView} view
|
|
294
|
+
* @param {number} offset
|
|
295
|
+
* @returns {number}
|
|
296
|
+
*/
|
|
297
|
+
static #readMil(view, offset) {
|
|
298
|
+
return view.getInt32(offset, true) / 10000
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Reads the little-endian record count from one stream header.
|
|
303
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
304
|
+
* @returns {number}
|
|
305
|
+
*/
|
|
306
|
+
static #readRecordCount(headerBytes) {
|
|
307
|
+
const normalizedHeader =
|
|
308
|
+
PcbPadPrimitiveParser.#toUint8Array(headerBytes)
|
|
309
|
+
|
|
310
|
+
if (normalizedHeader.byteLength < 4) {
|
|
311
|
+
return 0
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return new DataView(
|
|
315
|
+
normalizedHeader.buffer,
|
|
316
|
+
normalizedHeader.byteOffset,
|
|
317
|
+
4
|
|
318
|
+
).getUint32(0, true)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Reads one little-endian unsigned 32-bit value from a byte array.
|
|
323
|
+
* @param {Uint8Array} bytes
|
|
324
|
+
* @param {number} offset
|
|
325
|
+
* @returns {number}
|
|
326
|
+
*/
|
|
327
|
+
static #readUint32FromBytes(bytes, offset) {
|
|
328
|
+
return new DataView(
|
|
329
|
+
bytes.buffer,
|
|
330
|
+
bytes.byteOffset + offset,
|
|
331
|
+
4
|
|
332
|
+
).getUint32(0, true)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Converts an ArrayBuffer or view into a Uint8Array view.
|
|
337
|
+
* @param {Uint8Array | ArrayBuffer} bytes
|
|
338
|
+
* @returns {Uint8Array}
|
|
339
|
+
*/
|
|
340
|
+
static #toUint8Array(bytes) {
|
|
341
|
+
if (bytes instanceof Uint8Array) {
|
|
342
|
+
return bytes
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return new Uint8Array(bytes)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Maps Altium pad, drill, and pad-stack mode codes to stable model labels.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPadShapeCodec {
|
|
9
|
+
static #PAD_SHAPE_NAMES = new Map([
|
|
10
|
+
[0, 'none'],
|
|
11
|
+
[1, 'round'],
|
|
12
|
+
[2, 'rectangular'],
|
|
13
|
+
[3, 'octagonal'],
|
|
14
|
+
[9, 'rounded-rectangle']
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
static #HOLE_SHAPE_NAMES = new Map([
|
|
18
|
+
[-1, 'none'],
|
|
19
|
+
[0, 'round'],
|
|
20
|
+
[1, 'square'],
|
|
21
|
+
[2, 'slot']
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
static #PAD_MODE_NAMES = new Map([
|
|
25
|
+
[0, 'simple'],
|
|
26
|
+
[1, 'top-middle-bottom'],
|
|
27
|
+
[2, 'full-stack']
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a stable normalized label for one raw pad shape code.
|
|
32
|
+
* @param {number | null | undefined} shape
|
|
33
|
+
* @returns {string | null}
|
|
34
|
+
*/
|
|
35
|
+
static padShapeName(shape) {
|
|
36
|
+
return PcbPadShapeCodec.#mappedCodeName(
|
|
37
|
+
shape,
|
|
38
|
+
PcbPadShapeCodec.#PAD_SHAPE_NAMES
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns a stable normalized label for one raw drill-hole shape code.
|
|
44
|
+
* @param {number | null | undefined} shape
|
|
45
|
+
* @returns {string | null}
|
|
46
|
+
*/
|
|
47
|
+
static holeShapeName(shape) {
|
|
48
|
+
return PcbPadShapeCodec.#mappedCodeName(
|
|
49
|
+
shape,
|
|
50
|
+
PcbPadShapeCodec.#HOLE_SHAPE_NAMES
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a stable normalized label for one raw pad stack mode code.
|
|
56
|
+
* @param {number | null | undefined} mode
|
|
57
|
+
* @returns {string | null}
|
|
58
|
+
*/
|
|
59
|
+
static padModeName(mode) {
|
|
60
|
+
return PcbPadShapeCodec.#mappedCodeName(
|
|
61
|
+
mode,
|
|
62
|
+
PcbPadShapeCodec.#PAD_MODE_NAMES
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds normalized top/middle/bottom shape labels for one pad.
|
|
68
|
+
* @param {{ shapeTop?: number, shapeMid?: number, shapeBottom?: number }} pad
|
|
69
|
+
* @returns {{ shapeTopName: string | null, shapeMidName: string | null, shapeBottomName: string | null, padShapeNames: { top: string | null, middle: string | null, bottom: string | null } }}
|
|
70
|
+
*/
|
|
71
|
+
static describePadShapes(pad) {
|
|
72
|
+
const top = PcbPadShapeCodec.padShapeName(pad.shapeTop)
|
|
73
|
+
const middle = PcbPadShapeCodec.padShapeName(pad.shapeMid)
|
|
74
|
+
const bottom = PcbPadShapeCodec.padShapeName(pad.shapeBottom)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
shapeTopName: top,
|
|
78
|
+
shapeMidName: middle,
|
|
79
|
+
shapeBottomName: bottom,
|
|
80
|
+
padShapeNames: {
|
|
81
|
+
top,
|
|
82
|
+
middle,
|
|
83
|
+
bottom
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adds normalized names and an effective fallback to one middle-layer shape.
|
|
90
|
+
* @param {number} shape
|
|
91
|
+
* @param {number | null | undefined} fallbackShape
|
|
92
|
+
* @returns {{ shape: number, shapeName: string | null, effectiveShape: number, effectiveShapeName: string | null }}
|
|
93
|
+
*/
|
|
94
|
+
static describeMiddleLayerShape(shape, fallbackShape) {
|
|
95
|
+
const effectiveShape =
|
|
96
|
+
Number(shape) === 0 && Number.isFinite(Number(fallbackShape))
|
|
97
|
+
? Number(fallbackShape)
|
|
98
|
+
: Number(shape)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
shape: Number(shape),
|
|
102
|
+
shapeName: PcbPadShapeCodec.padShapeName(shape),
|
|
103
|
+
effectiveShape,
|
|
104
|
+
effectiveShapeName: PcbPadShapeCodec.padShapeName(effectiveShape)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds normalized slot or drill geometry for one parsed pad hole.
|
|
110
|
+
* @param {{ shape: number | null, diameter: number, slotLength: number | null, rotation: number | null }} hole
|
|
111
|
+
* @returns {{ shape: number, shapeName: string | null, diameter: number, slotLength: number | null, rotation: number | null, length: number, width: number } | null}
|
|
112
|
+
*/
|
|
113
|
+
static describeHoleGeometry(hole) {
|
|
114
|
+
if (hole.shape === null || hole.shape === undefined) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const diameter = Number(hole.diameter || 0)
|
|
119
|
+
const slotLength = Number(hole.slotLength || 0) || null
|
|
120
|
+
const length =
|
|
121
|
+
PcbPadShapeCodec.holeShapeName(hole.shape) === 'slot'
|
|
122
|
+
? Math.max(Number(slotLength || 0), diameter)
|
|
123
|
+
: diameter
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
shape: Number(hole.shape),
|
|
127
|
+
shapeName: PcbPadShapeCodec.holeShapeName(hole.shape),
|
|
128
|
+
diameter,
|
|
129
|
+
slotLength,
|
|
130
|
+
rotation: hole.rotation ?? null,
|
|
131
|
+
length,
|
|
132
|
+
width: diameter
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolves a code through a mapping table while preserving unknown values.
|
|
138
|
+
* @param {number | null | undefined} code
|
|
139
|
+
* @param {Map<number, string>} mapping
|
|
140
|
+
* @returns {string | null}
|
|
141
|
+
*/
|
|
142
|
+
static #mappedCodeName(code, mapping) {
|
|
143
|
+
if (code === null || code === undefined) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const numericCode = Number(code)
|
|
148
|
+
if (!Number.isFinite(numericCode)) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (mapping.has(numericCode)) {
|
|
153
|
+
return mapping.get(numericCode) ?? null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `unknown-${numericCode}`
|
|
157
|
+
}
|
|
158
|
+
}
|