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,192 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { AltiumParser } from './AltiumParser.mjs'
|
|
6
|
+
import { DraftsmanDigestParser } from './DraftsmanDigestParser.mjs'
|
|
7
|
+
import { PcbModelParser } from './PcbModelParser.mjs'
|
|
8
|
+
import { PrjPcbModelParser } from './PrjPcbModelParser.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Runs deterministic synthetic compatibility cases against parser entrypoints.
|
|
12
|
+
*/
|
|
13
|
+
export class ParserCompatibilityFuzzer {
|
|
14
|
+
static SCHEMA = 'altium-toolkit.parser-compatibility-fuzz.a1'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs all built-in synthetic parser compatibility cases.
|
|
18
|
+
* @returns {object}
|
|
19
|
+
*/
|
|
20
|
+
static run() {
|
|
21
|
+
const cases = ParserCompatibilityFuzzer.#cases().map((entry) =>
|
|
22
|
+
ParserCompatibilityFuzzer.#runCase(entry)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
schema: ParserCompatibilityFuzzer.SCHEMA,
|
|
27
|
+
summary: {
|
|
28
|
+
caseCount: cases.length,
|
|
29
|
+
failureCount: cases.filter((entry) => entry.status === 'fail')
|
|
30
|
+
.length,
|
|
31
|
+
diagnosticCount: cases.reduce(
|
|
32
|
+
(total, entry) =>
|
|
33
|
+
total + Number(entry.diagnosticCount || 0),
|
|
34
|
+
0
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
cases
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lists deterministic compatibility cases.
|
|
43
|
+
* @returns {{ key: string, parse: () => object }[]}
|
|
44
|
+
*/
|
|
45
|
+
static #cases() {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
key: 'sch-record-ordering',
|
|
49
|
+
parse: () =>
|
|
50
|
+
AltiumParser.parseArrayBufferToRendererModel(
|
|
51
|
+
'fuzz-order.SchDoc',
|
|
52
|
+
ParserCompatibilityFuzzer.#encodeText(
|
|
53
|
+
'|RECORD=999|Text=Unknown First|' +
|
|
54
|
+
'|HEADER=Schematic Document|' +
|
|
55
|
+
'|RECORD=31|CustomX=120|CustomY=80|BorderOn=F|TitleBlockOn=F|' +
|
|
56
|
+
'|RECORD=13|Location.X=10|Location.Y=10|Corner.X=80|Corner.Y=10|LineWidth=1|'
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'sch-odd-encoding',
|
|
62
|
+
parse: () =>
|
|
63
|
+
AltiumParser.parseArrayBufferToRendererModel(
|
|
64
|
+
'fuzz-encoding.SchDoc',
|
|
65
|
+
ParserCompatibilityFuzzer.#windows1252Schematic()
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: 'pcb-malformed-sidecars',
|
|
70
|
+
parse: () =>
|
|
71
|
+
PcbModelParser.parse('fuzz-sidecar.PcbDoc', [
|
|
72
|
+
{
|
|
73
|
+
sourceStream: 'Pads6/Data',
|
|
74
|
+
fields: {
|
|
75
|
+
X: 'not-a-number',
|
|
76
|
+
Y: '20mil',
|
|
77
|
+
HOLESIZE: 'malformed',
|
|
78
|
+
NET: 'NET_A'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
sourceStream: 'ExtendedPrimitiveInformation/Data',
|
|
83
|
+
fields: {
|
|
84
|
+
PRIMITIVEINDEX: 'not-a-number',
|
|
85
|
+
SolderMaskExpansionMode: 'Manual',
|
|
86
|
+
SolderMaskExpansion: 'bad'
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
sourceStream: 'UnsupportedSidecar/Data',
|
|
91
|
+
fields: { RECORD: '777', VALUE: 'preserve' }
|
|
92
|
+
}
|
|
93
|
+
])
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'project-sparse-documents',
|
|
97
|
+
parse: () =>
|
|
98
|
+
PrjPcbModelParser.parseText(
|
|
99
|
+
'fuzz-project.PrjPcb',
|
|
100
|
+
'[Design]\n\n[Document1]\nDocumentUniqueId=EMPTY\n'
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'draftsman-unsupported-container',
|
|
105
|
+
parse: () =>
|
|
106
|
+
DraftsmanDigestParser.parse(
|
|
107
|
+
'fuzz.PCBDwf',
|
|
108
|
+
new Uint8Array([0, 1, 2, 3]).buffer
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Executes one compatibility case.
|
|
116
|
+
* @param {{ key: string, parse: () => object }} entry Case descriptor.
|
|
117
|
+
* @returns {object}
|
|
118
|
+
*/
|
|
119
|
+
static #runCase(entry) {
|
|
120
|
+
try {
|
|
121
|
+
const model = entry.parse()
|
|
122
|
+
return {
|
|
123
|
+
key: entry.key,
|
|
124
|
+
status: 'pass',
|
|
125
|
+
kind: model?.kind || '',
|
|
126
|
+
fileType: model?.fileType || '',
|
|
127
|
+
diagnosticCount: (model?.diagnostics || []).length,
|
|
128
|
+
summary: ParserCompatibilityFuzzer.#stableSummary(
|
|
129
|
+
model?.summary || {}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
key: entry.key,
|
|
135
|
+
status: 'fail',
|
|
136
|
+
diagnosticCount: 1,
|
|
137
|
+
error: {
|
|
138
|
+
name: error?.name || 'Error',
|
|
139
|
+
message: error?.message || String(error)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds a stable compact summary object.
|
|
147
|
+
* @param {object} summary Parser summary.
|
|
148
|
+
* @returns {object}
|
|
149
|
+
*/
|
|
150
|
+
static #stableSummary(summary) {
|
|
151
|
+
return Object.fromEntries(
|
|
152
|
+
Object.entries(summary || {}).filter(([, value]) =>
|
|
153
|
+
['number', 'string', 'boolean'].includes(typeof value)
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Encodes text as UTF-8.
|
|
160
|
+
* @param {string} text Text payload.
|
|
161
|
+
* @returns {ArrayBuffer}
|
|
162
|
+
*/
|
|
163
|
+
static #encodeText(text) {
|
|
164
|
+
const bytes = new TextEncoder().encode(text)
|
|
165
|
+
return bytes.buffer.slice(
|
|
166
|
+
bytes.byteOffset,
|
|
167
|
+
bytes.byteOffset + bytes.length
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds a schematic payload with one Windows-1252 punctuation byte.
|
|
173
|
+
* @returns {ArrayBuffer}
|
|
174
|
+
*/
|
|
175
|
+
static #windows1252Schematic() {
|
|
176
|
+
const prefix = new TextEncoder().encode(
|
|
177
|
+
'|HEADER=Schematic Document|' +
|
|
178
|
+
'|RECORD=31|CustomX=120|CustomY=80|BorderOn=F|TitleBlockOn=F|' +
|
|
179
|
+
'|RECORD=4|Location.X=20|Location.Y=20|TEXT=ESD'
|
|
180
|
+
)
|
|
181
|
+
const suffix = new TextEncoder().encode('TVS|')
|
|
182
|
+
const bytes = new Uint8Array(prefix.length + 1 + suffix.length)
|
|
183
|
+
bytes.set(prefix, 0)
|
|
184
|
+
bytes[prefix.length] = 0x96
|
|
185
|
+
bytes.set(suffix, prefix.length + 1)
|
|
186
|
+
|
|
187
|
+
return bytes.buffer.slice(
|
|
188
|
+
bytes.byteOffset,
|
|
189
|
+
bytes.byteOffset + bytes.length
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes custom pad shape sidecars and links pads to authored geometry.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbCustomPadShapeParser {
|
|
11
|
+
static #SOURCE_STREAM = 'CustomShapes/Data'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses custom pad shape sidecar records.
|
|
15
|
+
* @param {Uint8Array | ArrayBuffer | undefined} dataBytes
|
|
16
|
+
* @param {string} [sourceStream]
|
|
17
|
+
* @returns {{ entries: object[], byPrimitiveIndex: Record<string, object[]> }}
|
|
18
|
+
*/
|
|
19
|
+
static parse(
|
|
20
|
+
dataBytes,
|
|
21
|
+
sourceStream = PcbCustomPadShapeParser.#SOURCE_STREAM
|
|
22
|
+
) {
|
|
23
|
+
const entries = PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
24
|
+
dataBytes,
|
|
25
|
+
sourceStream
|
|
26
|
+
)
|
|
27
|
+
.map((record) => PcbCustomPadShapeParser.#normalizeRecord(record))
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
const byPrimitiveIndex = {}
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const key = String(entry.primitiveIndex)
|
|
33
|
+
byPrimitiveIndex[key] = byPrimitiveIndex[key] || []
|
|
34
|
+
byPrimitiveIndex[key].push(entry)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
entries,
|
|
39
|
+
byPrimitiveIndex
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Links custom shape entries to pad records in place.
|
|
45
|
+
* @param {object[]} pads
|
|
46
|
+
* @param {{ byPrimitiveIndex?: Record<string, object[]> }} customShapes
|
|
47
|
+
* @param {{ regions?: object[], shapeBasedRegions?: object[], arcs?: object[], tracks?: object[], fills?: object[] }} geometry
|
|
48
|
+
*/
|
|
49
|
+
static attachToPads(pads, customShapes, geometry = {}) {
|
|
50
|
+
if (!Array.isArray(pads) || !customShapes?.byPrimitiveIndex) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let index = 0; index < pads.length; index += 1) {
|
|
55
|
+
const entries = customShapes.byPrimitiveIndex[String(index)] || []
|
|
56
|
+
if (!entries.length) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pads[index].customShape =
|
|
61
|
+
PcbCustomPadShapeParser.#buildPadCustomShape(
|
|
62
|
+
index,
|
|
63
|
+
entries,
|
|
64
|
+
geometry
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalizes one custom-shape sidecar record.
|
|
71
|
+
* @param {{ fields: Record<string, string>, sourceStream: string }} record
|
|
72
|
+
* @returns {object | null}
|
|
73
|
+
*/
|
|
74
|
+
static #normalizeRecord(record) {
|
|
75
|
+
const primitiveIndex = PcbSidecarRecordParser.parseInteger(
|
|
76
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
77
|
+
'PRIMITIVEINDEX',
|
|
78
|
+
'PADINDEX',
|
|
79
|
+
'ANCHORINDEX'
|
|
80
|
+
])
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (primitiveIndex === null) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
primitiveIndex,
|
|
89
|
+
layer: PcbSidecarRecordParser.firstField(record.fields, ['LAYER']),
|
|
90
|
+
layerId: PcbSidecarRecordParser.parseInteger(
|
|
91
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
92
|
+
'LAYERID',
|
|
93
|
+
'LAYERINDEX'
|
|
94
|
+
])
|
|
95
|
+
),
|
|
96
|
+
pasteMask: PcbSidecarRecordParser.parseBoolean(
|
|
97
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
98
|
+
'PASTEMASK',
|
|
99
|
+
'HASPASTEMASK'
|
|
100
|
+
])
|
|
101
|
+
),
|
|
102
|
+
solderMask: PcbSidecarRecordParser.parseBoolean(
|
|
103
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
104
|
+
'SOLDERMASK',
|
|
105
|
+
'HASSOLDERMASK'
|
|
106
|
+
])
|
|
107
|
+
),
|
|
108
|
+
regionIndexes: PcbCustomPadShapeParser.#parseIndexList(
|
|
109
|
+
record.fields,
|
|
110
|
+
['REGIONINDEX', 'REGIONINDEXES']
|
|
111
|
+
),
|
|
112
|
+
shapeRegionIndexes: PcbCustomPadShapeParser.#parseIndexList(
|
|
113
|
+
record.fields,
|
|
114
|
+
['SHAPEREGIONINDEX', 'SHAPEREGIONINDEXES']
|
|
115
|
+
),
|
|
116
|
+
arcIndexes: PcbCustomPadShapeParser.#parseIndexList(record.fields, [
|
|
117
|
+
'ARCINDEX',
|
|
118
|
+
'ARCINDEXES'
|
|
119
|
+
]),
|
|
120
|
+
trackIndexes: PcbCustomPadShapeParser.#parseIndexList(
|
|
121
|
+
record.fields,
|
|
122
|
+
['TRACKINDEX', 'TRACKINDEXES']
|
|
123
|
+
),
|
|
124
|
+
fillIndexes: PcbCustomPadShapeParser.#parseIndexList(
|
|
125
|
+
record.fields,
|
|
126
|
+
['FILLINDEX', 'FILLINDEXES']
|
|
127
|
+
),
|
|
128
|
+
sourceStream: record.sourceStream,
|
|
129
|
+
fields: record.fields
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds the custom-shape object attached to a pad.
|
|
135
|
+
* @param {number} primitiveIndex
|
|
136
|
+
* @param {object[]} entries
|
|
137
|
+
* @param {object} geometry
|
|
138
|
+
* @returns {object}
|
|
139
|
+
*/
|
|
140
|
+
static #buildPadCustomShape(primitiveIndex, entries, geometry) {
|
|
141
|
+
return {
|
|
142
|
+
primitiveIndex,
|
|
143
|
+
sourceStream: entries[0]?.sourceStream || '',
|
|
144
|
+
layers: entries.map((entry) =>
|
|
145
|
+
PcbCustomPadShapeParser.#buildLayerShape(entry, geometry)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolves one layer-specific shape entry.
|
|
152
|
+
* @param {object} entry
|
|
153
|
+
* @param {object} geometry
|
|
154
|
+
* @returns {object}
|
|
155
|
+
*/
|
|
156
|
+
static #buildLayerShape(entry, geometry) {
|
|
157
|
+
return {
|
|
158
|
+
layer: entry.layer,
|
|
159
|
+
layerId: entry.layerId,
|
|
160
|
+
pasteMask: entry.pasteMask,
|
|
161
|
+
solderMask: entry.solderMask,
|
|
162
|
+
regions: PcbCustomPadShapeParser.#lookupMany(
|
|
163
|
+
geometry.regions || [],
|
|
164
|
+
entry.regionIndexes
|
|
165
|
+
),
|
|
166
|
+
arcs: PcbCustomPadShapeParser.#lookupMany(
|
|
167
|
+
geometry.arcs || [],
|
|
168
|
+
entry.arcIndexes
|
|
169
|
+
),
|
|
170
|
+
tracks: PcbCustomPadShapeParser.#lookupMany(
|
|
171
|
+
geometry.tracks || [],
|
|
172
|
+
entry.trackIndexes
|
|
173
|
+
),
|
|
174
|
+
fills: PcbCustomPadShapeParser.#lookupMany(
|
|
175
|
+
geometry.fills || [],
|
|
176
|
+
entry.fillIndexes
|
|
177
|
+
),
|
|
178
|
+
...(entry.shapeRegionIndexes.length
|
|
179
|
+
? {
|
|
180
|
+
shapeRegions: PcbCustomPadShapeParser.#lookupMany(
|
|
181
|
+
geometry.shapeBasedRegions || [],
|
|
182
|
+
entry.shapeRegionIndexes
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
: {})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolves several primitive indexes into object references.
|
|
191
|
+
* @param {object[]} collection
|
|
192
|
+
* @param {number[]} indexes
|
|
193
|
+
* @returns {object[]}
|
|
194
|
+
*/
|
|
195
|
+
static #lookupMany(collection, indexes) {
|
|
196
|
+
return indexes
|
|
197
|
+
.map((index) => collection[index])
|
|
198
|
+
.filter((value) => value && typeof value === 'object')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parses one or more integer indexes from scalar or numbered fields.
|
|
203
|
+
* @param {Record<string, string>} fields
|
|
204
|
+
* @param {string[]} baseKeys
|
|
205
|
+
* @returns {number[]}
|
|
206
|
+
*/
|
|
207
|
+
static #parseIndexList(fields, baseKeys) {
|
|
208
|
+
const indexes = []
|
|
209
|
+
|
|
210
|
+
for (const baseKey of baseKeys) {
|
|
211
|
+
const directValue = fields[baseKey]
|
|
212
|
+
if (directValue) {
|
|
213
|
+
indexes.push(
|
|
214
|
+
...PcbCustomPadShapeParser.#splitIndexes(directValue)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
219
|
+
if (!key.startsWith(baseKey)) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (key === baseKey) {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
indexes.push(...PcbCustomPadShapeParser.#splitIndexes(value))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return [...new Set(indexes)]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parses a comma/semicolon-delimited index field.
|
|
235
|
+
* @param {string} value
|
|
236
|
+
* @returns {number[]}
|
|
237
|
+
*/
|
|
238
|
+
static #splitIndexes(value) {
|
|
239
|
+
return String(value || '')
|
|
240
|
+
.split(/[;,\s]+/u)
|
|
241
|
+
.map((part) => PcbSidecarRecordParser.parseInteger(part))
|
|
242
|
+
.filter(Number.isInteger)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getField, parseNumericField, toColor } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds read-only PCB and footprint-library default metadata.
|
|
11
|
+
*/
|
|
12
|
+
export class PcbDefaultsParser {
|
|
13
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.defaults.a1'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses a defaults read model from one field bag.
|
|
17
|
+
* @param {Record<string, string | string[]> | undefined} fields Source fields.
|
|
18
|
+
* @param {string} source Defaults source label.
|
|
19
|
+
* @returns {object | null}
|
|
20
|
+
*/
|
|
21
|
+
static parse(fields, source) {
|
|
22
|
+
const board = PcbDefaultsParser.#stripEmpty({
|
|
23
|
+
defaultFontName: PcbDefaultsParser.#firstField(fields, [
|
|
24
|
+
'DEFAULTFONTNAME',
|
|
25
|
+
'FONTNAME',
|
|
26
|
+
'TEXTFONTNAME'
|
|
27
|
+
]),
|
|
28
|
+
units: PcbDefaultsParser.#firstField(fields, [
|
|
29
|
+
'UNITS',
|
|
30
|
+
'BOARDUNITS',
|
|
31
|
+
'MEASUREMENTUNITS'
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
const primitiveStyles = PcbDefaultsParser.#stripEmpty({
|
|
35
|
+
trackWidthMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
36
|
+
'TRACKWIDTH',
|
|
37
|
+
'DEFAULTTRACKWIDTH',
|
|
38
|
+
'ROUTINGWIDTH'
|
|
39
|
+
]),
|
|
40
|
+
viaHoleSizeMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
41
|
+
'VIAHOLESIZE',
|
|
42
|
+
'VIAHOLE',
|
|
43
|
+
'DEFAULTVIAHOLESIZE'
|
|
44
|
+
]),
|
|
45
|
+
viaDiameterMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
46
|
+
'VIADIAMETER',
|
|
47
|
+
'VIASIZE',
|
|
48
|
+
'DEFAULTVIADIAMETER'
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
|
+
const maskPaste = PcbDefaultsParser.#stripEmpty({
|
|
52
|
+
solder: PcbDefaultsParser.#stripEmpty({
|
|
53
|
+
expansionMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
54
|
+
'SOLDERMASKEXPANSION',
|
|
55
|
+
'SOLDERMASKEXPANSION_DEFAULT',
|
|
56
|
+
'MASKEXPANSION'
|
|
57
|
+
])
|
|
58
|
+
}),
|
|
59
|
+
paste: PcbDefaultsParser.#stripEmpty({
|
|
60
|
+
expansionMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
61
|
+
'PASTEMASKEXPANSION',
|
|
62
|
+
'PASTEMASKEXPANSION_DEFAULT',
|
|
63
|
+
'PASTEEXPANSION'
|
|
64
|
+
])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
const clearances = PcbDefaultsParser.#stripEmpty({
|
|
68
|
+
defaultClearanceMil: PcbDefaultsParser.#firstNumber(fields, [
|
|
69
|
+
'CLEARANCE',
|
|
70
|
+
'DEFAULTCLEARANCE',
|
|
71
|
+
'MINCLEARANCE'
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
const colors = PcbDefaultsParser.#stripEmpty({
|
|
75
|
+
defaultColor: PcbDefaultsParser.#firstColor(fields, [
|
|
76
|
+
'DEFAULTCOLOR',
|
|
77
|
+
'COLOR'
|
|
78
|
+
]),
|
|
79
|
+
solderMaskTopColor: PcbDefaultsParser.#firstColor(fields, [
|
|
80
|
+
'SOLDERMASKTOPCOLOR',
|
|
81
|
+
'TOPSOLDERMASKCOLOR',
|
|
82
|
+
'CFG3D.TOPSOLDERMASKCOLOR'
|
|
83
|
+
]),
|
|
84
|
+
solderMaskBottomColor: PcbDefaultsParser.#firstColor(fields, [
|
|
85
|
+
'SOLDERMASKBOTTOMCOLOR',
|
|
86
|
+
'BOTTOMSOLDERMASKCOLOR',
|
|
87
|
+
'CFG3D.BOTTOMSOLDERMASKCOLOR'
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
const defaults = PcbDefaultsParser.#stripEmpty({
|
|
91
|
+
schema: PcbDefaultsParser.SCHEMA_ID,
|
|
92
|
+
source,
|
|
93
|
+
board,
|
|
94
|
+
primitiveStyles,
|
|
95
|
+
maskPaste,
|
|
96
|
+
clearances,
|
|
97
|
+
colors
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return Object.keys(defaults).length > 2 ? defaults : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the first populated field value.
|
|
105
|
+
* @param {Record<string, string | string[]> | undefined} fields Source fields.
|
|
106
|
+
* @param {string[]} keys Candidate keys.
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
static #firstField(fields, keys) {
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const value = getField(fields, key)
|
|
112
|
+
if (value) return value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return ''
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns the first parsed numeric field.
|
|
120
|
+
* @param {Record<string, string | string[]> | undefined} fields Source fields.
|
|
121
|
+
* @param {string[]} keys Candidate keys.
|
|
122
|
+
* @returns {number | undefined}
|
|
123
|
+
*/
|
|
124
|
+
static #firstNumber(fields, keys) {
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
const value = parseNumericField(fields, key)
|
|
127
|
+
if (value !== null) return value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns the first parsed color field.
|
|
135
|
+
* @param {Record<string, string | string[]> | undefined} fields Source fields.
|
|
136
|
+
* @param {string[]} keys Candidate keys.
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
static #firstColor(fields, keys) {
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
if (getField(fields, key)) return toColor(fields[key], '')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ''
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Removes empty properties from one object.
|
|
149
|
+
* @param {Record<string, unknown>} object Source object.
|
|
150
|
+
* @returns {Record<string, unknown>}
|
|
151
|
+
*/
|
|
152
|
+
static #stripEmpty(object) {
|
|
153
|
+
const result = {}
|
|
154
|
+
|
|
155
|
+
for (const [key, value] of Object.entries(object || {})) {
|
|
156
|
+
if (value === undefined || value === null || value === '') {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
typeof value === 'object' &&
|
|
161
|
+
!Array.isArray(value) &&
|
|
162
|
+
Object.keys(value).length === 0
|
|
163
|
+
) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
result[key] = value
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
}
|
|
171
|
+
}
|