altium-toolkit 1.0.8 → 1.0.10
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 +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1252 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a read-only PCB primitive ownership graph from normalized indexes.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbOwnershipGraphBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.ownership.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds primitive owner groups keyed by component, net, and polygon index.
|
|
13
|
+
* @param {{ components?: object[], nets?: object[], fills?: object[], tracks?: object[], arcs?: object[], vias?: object[], pads?: object[], regions?: object[], shapeBasedRegions?: object[], texts?: object[] }} pcb Normalized PCB model.
|
|
14
|
+
* @returns {{ schema: string, primitiveOwners: object[], componentsByIndex: Record<string, object>, netsByIndex: Record<string, object>, polygonsByIndex: Record<string, object> }}
|
|
15
|
+
*/
|
|
16
|
+
static build(pcb) {
|
|
17
|
+
const componentNames = PcbOwnershipGraphBuilder.#componentNames(
|
|
18
|
+
pcb?.components || []
|
|
19
|
+
)
|
|
20
|
+
const netNames = PcbOwnershipGraphBuilder.#netNames(pcb?.nets || [])
|
|
21
|
+
const componentsByIndex =
|
|
22
|
+
PcbOwnershipGraphBuilder.#initialComponentGroups(
|
|
23
|
+
pcb?.components || []
|
|
24
|
+
)
|
|
25
|
+
const netsByIndex = {}
|
|
26
|
+
const polygonsByIndex = {}
|
|
27
|
+
const primitiveOwners = []
|
|
28
|
+
|
|
29
|
+
for (const item of PcbOwnershipGraphBuilder.#primitiveItems(pcb)) {
|
|
30
|
+
const owner = PcbOwnershipGraphBuilder.#primitiveOwner(
|
|
31
|
+
item,
|
|
32
|
+
componentNames,
|
|
33
|
+
netNames
|
|
34
|
+
)
|
|
35
|
+
if (!owner) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
primitiveOwners.push(owner)
|
|
40
|
+
PcbOwnershipGraphBuilder.#addGroupKey(
|
|
41
|
+
componentsByIndex,
|
|
42
|
+
owner.componentIndex,
|
|
43
|
+
{
|
|
44
|
+
componentIndex: owner.componentIndex,
|
|
45
|
+
designator: owner.component || '',
|
|
46
|
+
primitiveKeys: []
|
|
47
|
+
},
|
|
48
|
+
owner.primitiveKey
|
|
49
|
+
)
|
|
50
|
+
PcbOwnershipGraphBuilder.#addGroupKey(
|
|
51
|
+
netsByIndex,
|
|
52
|
+
owner.netIndex,
|
|
53
|
+
{
|
|
54
|
+
netIndex: owner.netIndex,
|
|
55
|
+
name: owner.net || '',
|
|
56
|
+
primitiveKeys: []
|
|
57
|
+
},
|
|
58
|
+
owner.primitiveKey
|
|
59
|
+
)
|
|
60
|
+
PcbOwnershipGraphBuilder.#addGroupKey(
|
|
61
|
+
polygonsByIndex,
|
|
62
|
+
owner.polygonIndex,
|
|
63
|
+
{
|
|
64
|
+
polygonIndex: owner.polygonIndex,
|
|
65
|
+
primitiveKeys: []
|
|
66
|
+
},
|
|
67
|
+
owner.primitiveKey
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
schema: PcbOwnershipGraphBuilder.SCHEMA_ID,
|
|
73
|
+
primitiveOwners,
|
|
74
|
+
componentsByIndex,
|
|
75
|
+
netsByIndex,
|
|
76
|
+
polygonsByIndex
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds primitive iterable entries in stable renderer collection order.
|
|
82
|
+
* @param {object} pcb Normalized PCB model.
|
|
83
|
+
* @returns {{ primitiveKind: string, primitiveKey: string, primitive: object }[]}
|
|
84
|
+
*/
|
|
85
|
+
static #primitiveItems(pcb) {
|
|
86
|
+
return [
|
|
87
|
+
['fill', pcb?.fills || []],
|
|
88
|
+
['track', pcb?.tracks || []],
|
|
89
|
+
['arc', pcb?.arcs || []],
|
|
90
|
+
['via', pcb?.vias || []],
|
|
91
|
+
['pad', pcb?.pads || []],
|
|
92
|
+
['region', pcb?.regions || []],
|
|
93
|
+
['shape-based-region', pcb?.shapeBasedRegions || []],
|
|
94
|
+
['text', pcb?.texts || []],
|
|
95
|
+
['polygon', pcb?.polygons || []]
|
|
96
|
+
].flatMap(([primitiveKind, primitives]) =>
|
|
97
|
+
primitives.map((primitive, index) => ({
|
|
98
|
+
primitiveKind,
|
|
99
|
+
primitiveKey: primitiveKind + '-' + index,
|
|
100
|
+
primitive
|
|
101
|
+
}))
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builds a compact primitive owner row.
|
|
107
|
+
* @param {{ primitiveKind: string, primitiveKey: string, primitive: object }} item Primitive item.
|
|
108
|
+
* @param {Map<number, string>} componentNames Component names by native index.
|
|
109
|
+
* @param {Map<number, string>} netNames Net names by native index.
|
|
110
|
+
* @returns {object | null}
|
|
111
|
+
*/
|
|
112
|
+
static #primitiveOwner(item, componentNames, netNames) {
|
|
113
|
+
const componentIndex =
|
|
114
|
+
PcbOwnershipGraphBuilder.#optionalInteger(
|
|
115
|
+
item.primitive.componentIndex
|
|
116
|
+
) ??
|
|
117
|
+
(item.primitiveKind === 'text'
|
|
118
|
+
? PcbOwnershipGraphBuilder.#optionalInteger(
|
|
119
|
+
item.primitive.ownerIndex
|
|
120
|
+
)
|
|
121
|
+
: null)
|
|
122
|
+
const netIndex = PcbOwnershipGraphBuilder.#optionalInteger(
|
|
123
|
+
item.primitive.netIndex
|
|
124
|
+
)
|
|
125
|
+
const polygonIndex = PcbOwnershipGraphBuilder.#optionalInteger(
|
|
126
|
+
item.primitive.polygonIndex
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
componentIndex === null &&
|
|
131
|
+
netIndex === null &&
|
|
132
|
+
polygonIndex === null
|
|
133
|
+
) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
primitiveKey: item.primitiveKey,
|
|
139
|
+
primitiveKind: item.primitiveKind,
|
|
140
|
+
componentIndex,
|
|
141
|
+
component:
|
|
142
|
+
componentIndex === null
|
|
143
|
+
? ''
|
|
144
|
+
: componentNames.get(componentIndex) || '',
|
|
145
|
+
netIndex,
|
|
146
|
+
net: netIndex === null ? '' : netNames.get(netIndex) || '',
|
|
147
|
+
polygonIndex
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds component designator lookup by native component index.
|
|
153
|
+
* @param {object[]} components Component rows.
|
|
154
|
+
* @returns {Map<number, string>}
|
|
155
|
+
*/
|
|
156
|
+
static #componentNames(components) {
|
|
157
|
+
const names = new Map()
|
|
158
|
+
|
|
159
|
+
for (const component of components || []) {
|
|
160
|
+
const componentIndex = PcbOwnershipGraphBuilder.#optionalInteger(
|
|
161
|
+
component?.componentIndex
|
|
162
|
+
)
|
|
163
|
+
if (componentIndex !== null) {
|
|
164
|
+
names.set(componentIndex, String(component.designator || ''))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return names
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds net name lookup by native net index.
|
|
173
|
+
* @param {object[]} nets Net rows.
|
|
174
|
+
* @returns {Map<number, string>}
|
|
175
|
+
*/
|
|
176
|
+
static #netNames(nets) {
|
|
177
|
+
const names = new Map()
|
|
178
|
+
|
|
179
|
+
for (const net of nets || []) {
|
|
180
|
+
const netIndex = PcbOwnershipGraphBuilder.#optionalInteger(
|
|
181
|
+
net?.netIndex
|
|
182
|
+
)
|
|
183
|
+
if (netIndex !== null) {
|
|
184
|
+
names.set(netIndex, String(net.name || ''))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return names
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Creates empty component groups so consumers can inspect ownerless rows.
|
|
193
|
+
* @param {object[]} components Component rows.
|
|
194
|
+
* @returns {Record<string, object>}
|
|
195
|
+
*/
|
|
196
|
+
static #initialComponentGroups(components) {
|
|
197
|
+
const groups = {}
|
|
198
|
+
|
|
199
|
+
for (const component of components || []) {
|
|
200
|
+
const componentIndex = PcbOwnershipGraphBuilder.#optionalInteger(
|
|
201
|
+
component?.componentIndex
|
|
202
|
+
)
|
|
203
|
+
if (componentIndex !== null) {
|
|
204
|
+
groups[String(componentIndex)] = {
|
|
205
|
+
componentIndex,
|
|
206
|
+
designator: String(component.designator || ''),
|
|
207
|
+
primitiveKeys: []
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return groups
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Adds one primitive key to a numeric owner group.
|
|
217
|
+
* @param {Record<string, object>} groups Group map.
|
|
218
|
+
* @param {number | null} index Owner index.
|
|
219
|
+
* @param {object} fallbackGroup Group to create when missing.
|
|
220
|
+
* @param {string} primitiveKey Primitive key to append.
|
|
221
|
+
*/
|
|
222
|
+
static #addGroupKey(groups, index, fallbackGroup, primitiveKey) {
|
|
223
|
+
if (index === null) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const key = String(index)
|
|
228
|
+
if (!groups[key]) {
|
|
229
|
+
groups[key] = fallbackGroup
|
|
230
|
+
}
|
|
231
|
+
if (!groups[key].primitiveKeys.includes(primitiveKey)) {
|
|
232
|
+
groups[key].primitiveKeys.push(primitiveKey)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parses an optional integer value.
|
|
238
|
+
* @param {unknown} value Candidate value.
|
|
239
|
+
* @returns {number | null}
|
|
240
|
+
*/
|
|
241
|
+
static #optionalInteger(value) {
|
|
242
|
+
const parsed = Number(value)
|
|
243
|
+
return Number.isInteger(parsed) ? parsed : null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -35,19 +35,18 @@ export class PcbPadPrimitiveParser {
|
|
|
35
35
|
return []
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const records = PcbPadPrimitiveParser.#readPadRecordSequence(
|
|
39
|
+
normalizedData,
|
|
40
|
+
0,
|
|
41
|
+
count
|
|
42
|
+
)
|
|
39
43
|
const pads = []
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
offset
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
if (!record) {
|
|
48
|
-
return []
|
|
49
|
-
}
|
|
45
|
+
if (!records) {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
50
48
|
|
|
49
|
+
for (const record of records) {
|
|
51
50
|
const pad = PcbPadPrimitiveParser.#parsePadSubrecords(
|
|
52
51
|
record.subrecords
|
|
53
52
|
)
|
|
@@ -57,25 +56,69 @@ export class PcbPadPrimitiveParser {
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
pads.push(pad)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return pads
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads all expected pad records without recursive suffix validation.
|
|
66
|
+
* @param {Uint8Array} bytes
|
|
67
|
+
* @param {number} offset
|
|
68
|
+
* @param {number} count
|
|
69
|
+
* @returns {{ subrecords: DataView[], nextOffset: number }[] | null}
|
|
70
|
+
*/
|
|
71
|
+
static #readPadRecordSequence(bytes, offset, count) {
|
|
72
|
+
const firstRecord = PcbPadPrimitiveParser.#readPadRecordAt(
|
|
73
|
+
bytes,
|
|
74
|
+
offset
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (!firstRecord) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const records = [firstRecord]
|
|
82
|
+
const alternativeScanOffsets = [null]
|
|
83
|
+
let depth = 1
|
|
84
|
+
let scanOffset = firstRecord.nextOffset
|
|
85
|
+
|
|
86
|
+
while (depth < count) {
|
|
87
|
+
const candidate = PcbPadPrimitiveParser.#findNextPadRecordCandidate(
|
|
88
|
+
bytes,
|
|
89
|
+
scanOffset
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (!candidate) {
|
|
93
|
+
let foundAlternative = false
|
|
94
|
+
|
|
95
|
+
while (depth > 1 && !foundAlternative) {
|
|
96
|
+
depth -= 1
|
|
97
|
+
records.length = depth
|
|
98
|
+
|
|
99
|
+
const alternativeOffset = alternativeScanOffsets[depth]
|
|
100
|
+
alternativeScanOffsets.length = depth
|
|
101
|
+
|
|
102
|
+
if (alternativeOffset !== null) {
|
|
103
|
+
scanOffset = alternativeOffset
|
|
104
|
+
foundAlternative = true
|
|
105
|
+
}
|
|
72
106
|
}
|
|
73
107
|
|
|
74
|
-
|
|
108
|
+
if (!foundAlternative) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
continue
|
|
75
113
|
}
|
|
114
|
+
|
|
115
|
+
records[depth] = candidate.record
|
|
116
|
+
alternativeScanOffsets[depth] = candidate.alternativeOffset
|
|
117
|
+
depth += 1
|
|
118
|
+
scanOffset = candidate.record.nextOffset
|
|
76
119
|
}
|
|
77
120
|
|
|
78
|
-
return
|
|
121
|
+
return records
|
|
79
122
|
}
|
|
80
123
|
|
|
81
124
|
/**
|
|
@@ -149,31 +192,28 @@ export class PcbPadPrimitiveParser {
|
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
/**
|
|
152
|
-
* Finds the next pad record
|
|
195
|
+
* Finds the next readable pad record after optional unknown subrecords.
|
|
153
196
|
* @param {Uint8Array} bytes
|
|
154
197
|
* @param {number} offset
|
|
155
|
-
* @
|
|
156
|
-
* @returns {number | null}
|
|
198
|
+
* @returns {{ record: { subrecords: DataView[], nextOffset: number }, alternativeOffset: number | null } | null}
|
|
157
199
|
*/
|
|
158
|
-
static #
|
|
200
|
+
static #findNextPadRecordCandidate(bytes, offset) {
|
|
159
201
|
let cursor = offset
|
|
160
202
|
|
|
161
203
|
while (cursor < bytes.byteLength) {
|
|
162
|
-
|
|
163
|
-
PcbPadPrimitiveParser.#canReadPadRecordSequence(
|
|
164
|
-
bytes,
|
|
165
|
-
cursor,
|
|
166
|
-
remainingCount
|
|
167
|
-
)
|
|
168
|
-
) {
|
|
169
|
-
return cursor
|
|
170
|
-
}
|
|
171
|
-
|
|
204
|
+
const record = PcbPadPrimitiveParser.#readPadRecordAt(bytes, cursor)
|
|
172
205
|
const unknownSubrecord = PcbPadPrimitiveParser.#readSubrecordAt(
|
|
173
206
|
bytes,
|
|
174
207
|
cursor
|
|
175
208
|
)
|
|
176
209
|
|
|
210
|
+
if (record) {
|
|
211
|
+
return {
|
|
212
|
+
record,
|
|
213
|
+
alternativeOffset: unknownSubrecord?.nextOffset ?? null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
177
217
|
if (!unknownSubrecord) {
|
|
178
218
|
return null
|
|
179
219
|
}
|
|
@@ -184,33 +224,6 @@ export class PcbPadPrimitiveParser {
|
|
|
184
224
|
return null
|
|
185
225
|
}
|
|
186
226
|
|
|
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
227
|
/**
|
|
215
228
|
* Decodes one pad payload from its subrecords.
|
|
216
229
|
* @param {DataView[]} subrecords
|
|
@@ -48,6 +48,12 @@ export class PcbPadStackParser {
|
|
|
48
48
|
|
|
49
49
|
static #SOLDER_MASK_CACHE_VALID_OFFSET = 104
|
|
50
50
|
|
|
51
|
+
static #POSITIVE_TOLERANCE_OFFSET = 162
|
|
52
|
+
|
|
53
|
+
static #NEGATIVE_TOLERANCE_OFFSET = 166
|
|
54
|
+
|
|
55
|
+
static #HOLE_TOLERANCE_UNSET = 0x7fffffff
|
|
56
|
+
|
|
51
57
|
static #EXTENSION_MIN_BYTE_LENGTH = 596
|
|
52
58
|
|
|
53
59
|
static #INNER_LAYER_COUNT = 29
|
|
@@ -75,6 +81,10 @@ export class PcbPadStackParser {
|
|
|
75
81
|
const flags = PcbPadStackParser.#parseFlags(mainRecord)
|
|
76
82
|
const mainRecordTail =
|
|
77
83
|
PcbPadStackParser.#parseMainRecordTail(mainRecord)
|
|
84
|
+
const extension = PcbPadStackParser.#parseExtensionRecord(
|
|
85
|
+
extensionRecord,
|
|
86
|
+
padContext
|
|
87
|
+
)
|
|
78
88
|
|
|
79
89
|
return {
|
|
80
90
|
...flags,
|
|
@@ -84,8 +94,10 @@ export class PcbPadStackParser {
|
|
|
84
94
|
mainRecordTail,
|
|
85
95
|
padContext
|
|
86
96
|
),
|
|
87
|
-
...
|
|
88
|
-
|
|
97
|
+
...extension,
|
|
98
|
+
...PcbPadStackParser.#buildLocalStack(
|
|
99
|
+
mainRecordTail,
|
|
100
|
+
extension,
|
|
89
101
|
padContext
|
|
90
102
|
)
|
|
91
103
|
}
|
|
@@ -178,6 +190,7 @@ export class PcbPadStackParser {
|
|
|
178
190
|
|
|
179
191
|
PcbPadStackParser.#assignPadCacheFields(result, mainRecord)
|
|
180
192
|
PcbPadStackParser.#assignMaskCacheFields(result, mainRecord)
|
|
193
|
+
PcbPadStackParser.#assignHoleToleranceFields(result, mainRecord)
|
|
181
194
|
|
|
182
195
|
return result
|
|
183
196
|
}
|
|
@@ -292,6 +305,35 @@ export class PcbPadStackParser {
|
|
|
292
305
|
}
|
|
293
306
|
}
|
|
294
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Adds optional hole tolerance fields to an output object.
|
|
310
|
+
* @param {Record<string, unknown>} result
|
|
311
|
+
* @param {DataView} mainRecord
|
|
312
|
+
*/
|
|
313
|
+
static #assignHoleToleranceFields(result, mainRecord) {
|
|
314
|
+
const positiveTolerance = PcbPadStackParser.#readHoleTolerance(
|
|
315
|
+
mainRecord,
|
|
316
|
+
PcbPadStackParser.#POSITIVE_TOLERANCE_OFFSET
|
|
317
|
+
)
|
|
318
|
+
const negativeTolerance = PcbPadStackParser.#readHoleTolerance(
|
|
319
|
+
mainRecord,
|
|
320
|
+
PcbPadStackParser.#NEGATIVE_TOLERANCE_OFFSET
|
|
321
|
+
)
|
|
322
|
+
const holeTolerance = {}
|
|
323
|
+
|
|
324
|
+
if (positiveTolerance !== null) {
|
|
325
|
+
result.positiveTolerance = positiveTolerance
|
|
326
|
+
holeTolerance.positive = positiveTolerance
|
|
327
|
+
}
|
|
328
|
+
if (negativeTolerance !== null) {
|
|
329
|
+
result.negativeTolerance = negativeTolerance
|
|
330
|
+
holeTolerance.negative = negativeTolerance
|
|
331
|
+
}
|
|
332
|
+
if (Object.keys(holeTolerance).length) {
|
|
333
|
+
result.holeTolerance = holeTolerance
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
295
337
|
/**
|
|
296
338
|
* Returns whether one decoded pad cache contains meaningful data.
|
|
297
339
|
* @param {{ planeConnectionStyle: number, thermalReliefConductorWidth: number, thermalReliefConductorCount: number, thermalReliefAirGap: number, powerPlaneReliefExpansion: number, powerPlaneClearance: number, validity: Record<string, number> }} padCache
|
|
@@ -311,6 +353,28 @@ export class PcbPadStackParser {
|
|
|
311
353
|
return values.some((value) => value !== 0)
|
|
312
354
|
}
|
|
313
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Reads one optional hole tolerance from a pad main record.
|
|
358
|
+
* @param {DataView} mainRecord
|
|
359
|
+
* @param {number} offset
|
|
360
|
+
* @returns {number | null}
|
|
361
|
+
*/
|
|
362
|
+
static #readHoleTolerance(mainRecord, offset) {
|
|
363
|
+
if (!mainRecord || offset + 4 > mainRecord.byteLength) {
|
|
364
|
+
return null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const rawValue = mainRecord.getInt32(offset, true)
|
|
368
|
+
if (
|
|
369
|
+
rawValue === 0 ||
|
|
370
|
+
rawValue === PcbPadStackParser.#HOLE_TOLERANCE_UNSET
|
|
371
|
+
) {
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return rawValue / 10000
|
|
376
|
+
}
|
|
377
|
+
|
|
314
378
|
/**
|
|
315
379
|
* Adds derived mask-expansion and layer-opening semantics.
|
|
316
380
|
* @param {Record<string, boolean | number>} flags
|
|
@@ -664,6 +728,169 @@ export class PcbPadStackParser {
|
|
|
664
728
|
}
|
|
665
729
|
}
|
|
666
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Builds a normalized local-stack geometry read model.
|
|
733
|
+
* @param {Record<string, boolean | number>} mainRecordTail Main tail fields.
|
|
734
|
+
* @param {Record<string, unknown>} extension Extension fields.
|
|
735
|
+
* @param {Record<string, unknown>} padContext Parsed pad fields.
|
|
736
|
+
* @returns {{ localStack?: object }}
|
|
737
|
+
*/
|
|
738
|
+
static #buildLocalStack(mainRecordTail, extension, padContext) {
|
|
739
|
+
const mode = Number(mainRecordTail.padMode)
|
|
740
|
+
if (mode === 1) {
|
|
741
|
+
return {
|
|
742
|
+
localStack: {
|
|
743
|
+
schema: 'altium-toolkit.pcb.pad-local-stack.a1',
|
|
744
|
+
mode,
|
|
745
|
+
modeName: String(mainRecordTail.padModeName || ''),
|
|
746
|
+
source: 'main-record',
|
|
747
|
+
layers: [
|
|
748
|
+
PcbPadStackParser.#localStackLayer(
|
|
749
|
+
'top',
|
|
750
|
+
1,
|
|
751
|
+
'L1',
|
|
752
|
+
padContext,
|
|
753
|
+
extension
|
|
754
|
+
),
|
|
755
|
+
PcbPadStackParser.#localStackLayer(
|
|
756
|
+
'middle',
|
|
757
|
+
null,
|
|
758
|
+
'INNER',
|
|
759
|
+
padContext,
|
|
760
|
+
extension
|
|
761
|
+
),
|
|
762
|
+
PcbPadStackParser.#localStackLayer(
|
|
763
|
+
'bottom',
|
|
764
|
+
32,
|
|
765
|
+
'L32',
|
|
766
|
+
padContext,
|
|
767
|
+
extension
|
|
768
|
+
)
|
|
769
|
+
],
|
|
770
|
+
hole: PcbPadStackParser.#localStackHole(
|
|
771
|
+
padContext,
|
|
772
|
+
extension
|
|
773
|
+
)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (
|
|
779
|
+
mode === 2 &&
|
|
780
|
+
Array.isArray(extension.fullStackLayerEntries) &&
|
|
781
|
+
extension.fullStackLayerEntries.length
|
|
782
|
+
) {
|
|
783
|
+
return {
|
|
784
|
+
localStack: {
|
|
785
|
+
schema: 'altium-toolkit.pcb.pad-local-stack.a1',
|
|
786
|
+
mode,
|
|
787
|
+
modeName: String(mainRecordTail.padModeName || ''),
|
|
788
|
+
source: 'extension-record',
|
|
789
|
+
layers: extension.fullStackLayerEntries.map((entry) => ({
|
|
790
|
+
role: 'layer',
|
|
791
|
+
layerId: Number(entry.layerCode),
|
|
792
|
+
layerKey: 'L' + Number(entry.layerCode),
|
|
793
|
+
enabled: entry.enabled,
|
|
794
|
+
width: entry.sizeX,
|
|
795
|
+
height: entry.sizeY,
|
|
796
|
+
cornerRadius: entry.cornerRadius,
|
|
797
|
+
modeFlags: entry.modeFlags
|
|
798
|
+
})),
|
|
799
|
+
hole: PcbPadStackParser.#localStackHole(
|
|
800
|
+
padContext,
|
|
801
|
+
extension
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return {}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Builds one top/middle/bottom local-stack layer entry.
|
|
812
|
+
* @param {'top' | 'middle' | 'bottom'} role Layer role.
|
|
813
|
+
* @param {number | null} layerId Layer id.
|
|
814
|
+
* @param {string} layerKey Stable layer key.
|
|
815
|
+
* @param {Record<string, unknown>} padContext Parsed pad fields.
|
|
816
|
+
* @param {Record<string, unknown>} extension Extension fields.
|
|
817
|
+
* @returns {object}
|
|
818
|
+
*/
|
|
819
|
+
static #localStackLayer(role, layerId, layerKey, padContext, extension) {
|
|
820
|
+
const suffix =
|
|
821
|
+
role === 'top' ? 'Top' : role === 'bottom' ? 'Bottom' : 'Mid'
|
|
822
|
+
const offset = PcbPadStackParser.#layerOffset(role, extension)
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
role,
|
|
826
|
+
layerId,
|
|
827
|
+
layerKey,
|
|
828
|
+
width: Number(padContext['size' + suffix + 'X'] || 0),
|
|
829
|
+
height: Number(padContext['size' + suffix + 'Y'] || 0),
|
|
830
|
+
shape: PcbPadStackParser.#numericOrNull(
|
|
831
|
+
padContext['shape' + suffix]
|
|
832
|
+
),
|
|
833
|
+
shapeName: PcbPadShapeCodec.padShapeName(
|
|
834
|
+
padContext['shape' + suffix]
|
|
835
|
+
),
|
|
836
|
+
offsetX: offset.x,
|
|
837
|
+
offsetY: offset.y
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Resolves layer offsets from extension data when present.
|
|
843
|
+
* @param {'top' | 'middle' | 'bottom'} role Layer role.
|
|
844
|
+
* @param {Record<string, unknown>} extension Extension fields.
|
|
845
|
+
* @returns {{ x: number, y: number }}
|
|
846
|
+
*/
|
|
847
|
+
static #layerOffset(role, extension) {
|
|
848
|
+
const layerNumber = role === 'top' ? 1 : role === 'bottom' ? 32 : null
|
|
849
|
+
const offset = Array.isArray(extension.layerOffsets)
|
|
850
|
+
? extension.layerOffsets.find(
|
|
851
|
+
(entry) => entry.layerNumber === layerNumber
|
|
852
|
+
)
|
|
853
|
+
: null
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
x: Number(offset?.x || 0),
|
|
857
|
+
y: Number(offset?.y || 0)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Builds local-stack hole geometry.
|
|
863
|
+
* @param {Record<string, unknown>} padContext Parsed pad fields.
|
|
864
|
+
* @param {Record<string, unknown>} extension Extension fields.
|
|
865
|
+
* @returns {object}
|
|
866
|
+
*/
|
|
867
|
+
static #localStackHole(padContext, extension) {
|
|
868
|
+
const shape = PcbPadStackParser.#numericOrNull(extension.holeShape)
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
diameter: Number(padContext.holeDiameter || 0),
|
|
872
|
+
shape,
|
|
873
|
+
shapeName:
|
|
874
|
+
shape === null ? null : PcbPadShapeCodec.holeShapeName(shape),
|
|
875
|
+
slotLength: extension.holeSlotLength ?? null,
|
|
876
|
+
rotation: extension.holeRotation ?? null
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Converts finite numeric values and nullish values into stable output.
|
|
882
|
+
* @param {unknown} value Candidate value.
|
|
883
|
+
* @returns {number | null}
|
|
884
|
+
*/
|
|
885
|
+
static #numericOrNull(value) {
|
|
886
|
+
if (value === null || value === undefined || value === '') {
|
|
887
|
+
return null
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const number = Number(value)
|
|
891
|
+
return Number.isFinite(number) ? number : null
|
|
892
|
+
}
|
|
893
|
+
|
|
667
894
|
/**
|
|
668
895
|
* Decodes non-empty inner-layer pad sizes.
|
|
669
896
|
* @param {DataView} extensionRecord
|