altium-toolkit 1.1.3 → 1.1.23
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 +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classifies Altium PCB layer identifiers into stable public groups.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbLayerGroups {
|
|
9
|
+
static #GROUP_PRESENTATION = {
|
|
10
|
+
'top-copper': { color: '#c05032', drawPriority: 600 },
|
|
11
|
+
'mid-copper': { color: '#8e6bbf', drawPriority: 550 },
|
|
12
|
+
'bottom-copper': { color: '#2f6f9f', drawPriority: 500 },
|
|
13
|
+
'internal-plane': { color: '#7a8f2a', drawPriority: 450 },
|
|
14
|
+
overlay: { color: '#f5f7fa', drawPriority: 900 },
|
|
15
|
+
paste: { color: '#b9c0c7', drawPriority: 800 },
|
|
16
|
+
'solder-mask': { color: '#2ca25f', drawPriority: 700 },
|
|
17
|
+
drill: { color: '#3c4043', drawPriority: 300 },
|
|
18
|
+
'drill-hole': { color: '#202124', drawPriority: 950 },
|
|
19
|
+
keepout: { color: '#d93025', drawPriority: 1000 },
|
|
20
|
+
mechanical: { color: '#7f8c8d', drawPriority: 100 },
|
|
21
|
+
'multi-layer': { color: '#8f5bd3', drawPriority: 650 },
|
|
22
|
+
unknown: { color: '#9aa0a6', drawPriority: 0 }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns true when the layer is top copper.
|
|
27
|
+
* @param {unknown} layerId Layer identifier.
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
static isTopCopper(layerId) {
|
|
31
|
+
return PcbLayerGroups.#layerId(layerId) === 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when the layer is bottom copper.
|
|
36
|
+
* @param {unknown} layerId Layer identifier.
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
static isBottomCopper(layerId) {
|
|
40
|
+
return PcbLayerGroups.#layerId(layerId) === 32
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true when the layer is an internal signal layer.
|
|
45
|
+
* @param {unknown} layerId Layer identifier.
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
static isMidCopper(layerId) {
|
|
49
|
+
const layer = PcbLayerGroups.#layerId(layerId)
|
|
50
|
+
return layer >= 2 && layer <= 31
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns true when the layer is any signal copper layer.
|
|
55
|
+
* @param {unknown} layerId Layer identifier.
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
static isCopper(layerId) {
|
|
59
|
+
const layer = PcbLayerGroups.#layerId(layerId)
|
|
60
|
+
return layer >= 1 && layer <= 32
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns true when the layer is an internal plane.
|
|
65
|
+
* @param {unknown} layerId Layer identifier.
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
static isInternalPlane(layerId) {
|
|
69
|
+
const layer = PcbLayerGroups.#layerId(layerId)
|
|
70
|
+
return layer >= 39 && layer <= 54
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns true when the layer is a silkscreen overlay.
|
|
75
|
+
* @param {unknown} layerId Layer identifier.
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
static isOverlay(layerId) {
|
|
79
|
+
return [33, 34].includes(PcbLayerGroups.#layerId(layerId))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns true when the layer is a solder paste layer.
|
|
84
|
+
* @param {unknown} layerId Layer identifier.
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
static isPaste(layerId) {
|
|
88
|
+
return [35, 36].includes(PcbLayerGroups.#layerId(layerId))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns true when the layer is a solder mask layer.
|
|
93
|
+
* @param {unknown} layerId Layer identifier.
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
static isSolderMask(layerId) {
|
|
97
|
+
return [37, 38].includes(PcbLayerGroups.#layerId(layerId))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns true when the layer is mechanical.
|
|
102
|
+
* @param {unknown} layerId Layer identifier.
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
static isMechanical(layerId) {
|
|
106
|
+
const layer = PcbLayerGroups.#layerId(layerId)
|
|
107
|
+
return layer >= 57 && layer <= 72
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns true when the layer carries drill drawing/guide output.
|
|
112
|
+
* @param {unknown} layerId Layer identifier.
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
static isDrill(layerId) {
|
|
116
|
+
return [55, 73].includes(PcbLayerGroups.#layerId(layerId))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns true when the layer represents a pad or via hole helper layer.
|
|
121
|
+
* @param {unknown} layerId Layer identifier.
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
static isDrillHole(layerId) {
|
|
125
|
+
return [81, 82].includes(PcbLayerGroups.#layerId(layerId))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns true when the layer spans multiple copper layers.
|
|
130
|
+
* @param {unknown} layerId Layer identifier.
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
static isMultiLayer(layerId) {
|
|
134
|
+
return PcbLayerGroups.#layerId(layerId) === 74
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns true when the layer is the keepout layer.
|
|
139
|
+
* @param {unknown} layerId Layer identifier.
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
static isKeepout(layerId) {
|
|
143
|
+
return PcbLayerGroups.#layerId(layerId) === 56
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns true for electrically meaningful layers plus silkscreen.
|
|
148
|
+
* @param {unknown} layerId Layer identifier.
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
static isSignalOrSilk(layerId) {
|
|
152
|
+
return (
|
|
153
|
+
PcbLayerGroups.isCopper(layerId) ||
|
|
154
|
+
PcbLayerGroups.isInternalPlane(layerId) ||
|
|
155
|
+
PcbLayerGroups.isOverlay(layerId) ||
|
|
156
|
+
PcbLayerGroups.isMultiLayer(layerId)
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolves one stable group name for a layer id.
|
|
162
|
+
* @param {unknown} layerId Layer identifier.
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
static groupForLayerId(layerId) {
|
|
166
|
+
if (PcbLayerGroups.isTopCopper(layerId)) return 'top-copper'
|
|
167
|
+
if (PcbLayerGroups.isBottomCopper(layerId)) return 'bottom-copper'
|
|
168
|
+
if (PcbLayerGroups.isMidCopper(layerId)) return 'mid-copper'
|
|
169
|
+
if (PcbLayerGroups.isInternalPlane(layerId)) return 'internal-plane'
|
|
170
|
+
if (PcbLayerGroups.isOverlay(layerId)) return 'overlay'
|
|
171
|
+
if (PcbLayerGroups.isPaste(layerId)) return 'paste'
|
|
172
|
+
if (PcbLayerGroups.isSolderMask(layerId)) return 'solder-mask'
|
|
173
|
+
if (PcbLayerGroups.isDrill(layerId)) return 'drill'
|
|
174
|
+
if (PcbLayerGroups.isDrillHole(layerId)) return 'drill-hole'
|
|
175
|
+
if (PcbLayerGroups.isKeepout(layerId)) return 'keepout'
|
|
176
|
+
if (PcbLayerGroups.isMechanical(layerId)) return 'mechanical'
|
|
177
|
+
if (PcbLayerGroups.isMultiLayer(layerId)) return 'multi-layer'
|
|
178
|
+
return 'unknown'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Describes one layer for diagnostics and reports.
|
|
183
|
+
* @param {unknown} layerId Layer identifier.
|
|
184
|
+
* @returns {{ layerId: number | null, group: string, side?: string, signalOrSilk: boolean }}
|
|
185
|
+
*/
|
|
186
|
+
static describeLayer(layerId) {
|
|
187
|
+
const normalizedLayerId = PcbLayerGroups.#layerId(layerId)
|
|
188
|
+
return PcbLayerGroups.#stripUndefined({
|
|
189
|
+
layerId: normalizedLayerId,
|
|
190
|
+
group: PcbLayerGroups.groupForLayerId(layerId),
|
|
191
|
+
side: PcbLayerGroups.#sideForLayerId(normalizedLayerId),
|
|
192
|
+
signalOrSilk: PcbLayerGroups.isSignalOrSilk(layerId)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves a deterministic display color for a layer id.
|
|
198
|
+
* @param {unknown} layerId Layer identifier.
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
static colorForLayerId(layerId) {
|
|
202
|
+
return PcbLayerGroups.#presentationForGroup(
|
|
203
|
+
PcbLayerGroups.groupForLayerId(layerId)
|
|
204
|
+
).color
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolves the deterministic draw priority for a layer id.
|
|
209
|
+
* @param {unknown} layerId Layer identifier.
|
|
210
|
+
* @returns {number}
|
|
211
|
+
*/
|
|
212
|
+
static drawPriorityForLayerId(layerId) {
|
|
213
|
+
return PcbLayerGroups.#presentationForGroup(
|
|
214
|
+
PcbLayerGroups.groupForLayerId(layerId)
|
|
215
|
+
).drawPriority
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Describes grouping, side, color, and draw priority for one layer id.
|
|
220
|
+
* @param {unknown} layerId Layer identifier.
|
|
221
|
+
* @returns {{ layerId: number | null, group: string, side?: string, signalOrSilk: boolean, color: string, drawPriority: number }}
|
|
222
|
+
*/
|
|
223
|
+
static presentationForLayerId(layerId) {
|
|
224
|
+
return {
|
|
225
|
+
...PcbLayerGroups.describeLayer(layerId),
|
|
226
|
+
color: PcbLayerGroups.colorForLayerId(layerId),
|
|
227
|
+
drawPriority: PcbLayerGroups.drawPriorityForLayerId(layerId)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sorts layer ids from lowest to highest deterministic draw priority.
|
|
233
|
+
* @param {unknown[]} layerIds Layer identifiers.
|
|
234
|
+
* @returns {unknown[]}
|
|
235
|
+
*/
|
|
236
|
+
static sortByDrawPriority(layerIds) {
|
|
237
|
+
return [...(Array.isArray(layerIds) ? layerIds : [])].sort(
|
|
238
|
+
(left, right) => {
|
|
239
|
+
const priorityDelta =
|
|
240
|
+
PcbLayerGroups.drawPriorityForLayerId(left) -
|
|
241
|
+
PcbLayerGroups.drawPriorityForLayerId(right)
|
|
242
|
+
|
|
243
|
+
if (priorityDelta !== 0) return priorityDelta
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
Number(PcbLayerGroups.#layerId(left) ?? 0) -
|
|
247
|
+
Number(PcbLayerGroups.#layerId(right) ?? 0)
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolves the physical side represented by one layer id.
|
|
255
|
+
* @param {number | null} layerId Normalized layer id.
|
|
256
|
+
* @returns {string | undefined}
|
|
257
|
+
*/
|
|
258
|
+
static #sideForLayerId(layerId) {
|
|
259
|
+
if ([1, 33, 35, 37].includes(layerId)) return 'top'
|
|
260
|
+
if ([32, 34, 36, 38].includes(layerId)) return 'bottom'
|
|
261
|
+
if (
|
|
262
|
+
PcbLayerGroups.isMidCopper(layerId) ||
|
|
263
|
+
PcbLayerGroups.isInternalPlane(layerId)
|
|
264
|
+
) {
|
|
265
|
+
return 'internal'
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
PcbLayerGroups.isMultiLayer(layerId) ||
|
|
269
|
+
PcbLayerGroups.isDrillHole(layerId)
|
|
270
|
+
) {
|
|
271
|
+
return 'all'
|
|
272
|
+
}
|
|
273
|
+
return undefined
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolves presentation metadata for one stable group.
|
|
278
|
+
* @param {string} group Layer group name.
|
|
279
|
+
* @returns {{ color: string, drawPriority: number }}
|
|
280
|
+
*/
|
|
281
|
+
static #presentationForGroup(group) {
|
|
282
|
+
return (
|
|
283
|
+
PcbLayerGroups.#GROUP_PRESENTATION[group] ||
|
|
284
|
+
PcbLayerGroups.#GROUP_PRESENTATION.unknown
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Normalizes one layer id.
|
|
290
|
+
* @param {unknown} layerId Layer identifier.
|
|
291
|
+
* @returns {number | null}
|
|
292
|
+
*/
|
|
293
|
+
static #layerId(layerId) {
|
|
294
|
+
const normalized = Number(layerId)
|
|
295
|
+
return Number.isInteger(normalized) ? normalized : null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Removes undefined fields from a report row.
|
|
300
|
+
* @param {object} row Report row.
|
|
301
|
+
* @returns {object}
|
|
302
|
+
*/
|
|
303
|
+
static #stripUndefined(row) {
|
|
304
|
+
return Object.fromEntries(
|
|
305
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { unzlibSync } from 'fflate'
|
|
6
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
|
+
import { PcbLayerStackInterchangeParser } from './PcbLayerStackInterchangeParser.mjs'
|
|
8
|
+
|
|
9
|
+
const { getField } = ParserUtils
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts compressed stack custom data from PCB board records.
|
|
13
|
+
*/
|
|
14
|
+
export class PcbLayerStackCustomDataParser {
|
|
15
|
+
/**
|
|
16
|
+
* Appends decoded stack custom data records after native board records.
|
|
17
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} boardRecords Board records.
|
|
18
|
+
* @returns {{ fields: Record<string, string>, sourceStream?: string, stackCustomData: true }[]}
|
|
19
|
+
*/
|
|
20
|
+
static parseBoardRecords(boardRecords) {
|
|
21
|
+
return PcbLayerStackCustomDataParser.parseBoardRecordsWithDiagnostics(
|
|
22
|
+
boardRecords
|
|
23
|
+
).records
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decodes stack custom data records with caller-visible diagnostics.
|
|
28
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} boardRecords Board records.
|
|
29
|
+
* @returns {{ records: { fields: Record<string, string>, sourceStream?: string, stackCustomData: true }[], diagnostics: object[] }}
|
|
30
|
+
*/
|
|
31
|
+
static parseBoardRecordsWithDiagnostics(boardRecords) {
|
|
32
|
+
const records = []
|
|
33
|
+
const diagnostics = []
|
|
34
|
+
|
|
35
|
+
for (const record of boardRecords || []) {
|
|
36
|
+
const parsed =
|
|
37
|
+
PcbLayerStackCustomDataParser.#parseBoardRecord(record)
|
|
38
|
+
|
|
39
|
+
if (parsed.record) {
|
|
40
|
+
records.push(parsed.record)
|
|
41
|
+
}
|
|
42
|
+
diagnostics.push(...parsed.diagnostics)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { records, diagnostics }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses one board record.
|
|
50
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }} record Board record.
|
|
51
|
+
* @returns {{ record: { fields: Record<string, string>, sourceStream?: string, stackCustomData: true } | null, diagnostics: object[] }}
|
|
52
|
+
*/
|
|
53
|
+
static #parseBoardRecord(record) {
|
|
54
|
+
const encoded = PcbLayerStackCustomDataParser.#encodedField(
|
|
55
|
+
record?.fields
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (!encoded.value) {
|
|
59
|
+
return { record: null, diagnostics: [] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const decoded = PcbLayerStackCustomDataParser.#decodePayload(
|
|
63
|
+
encoded.value
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (decoded.error) {
|
|
67
|
+
return {
|
|
68
|
+
record: null,
|
|
69
|
+
diagnostics: [
|
|
70
|
+
PcbLayerStackCustomDataParser.#decodeDiagnostic(
|
|
71
|
+
record,
|
|
72
|
+
encoded.name
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!decoded.text) {
|
|
79
|
+
return { record: null, diagnostics: [] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fields = PcbLayerStackInterchangeParser.parseTextToFields(
|
|
83
|
+
decoded.text,
|
|
84
|
+
{ format: 'stackupx' }
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (!Object.keys(fields).length) {
|
|
88
|
+
return { record: null, diagnostics: [] }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
record: {
|
|
93
|
+
sourceStream: record?.sourceStream,
|
|
94
|
+
fields,
|
|
95
|
+
stackCustomData: true
|
|
96
|
+
},
|
|
97
|
+
diagnostics: []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Finds the first stack custom data field.
|
|
103
|
+
* @param {Record<string, string | string[]> | undefined} fields Board fields.
|
|
104
|
+
* @returns {{ name: string, value: string }}
|
|
105
|
+
*/
|
|
106
|
+
static #encodedField(fields) {
|
|
107
|
+
const v9Value = getField(fields, 'V9_STACKCUSTOMDATA')
|
|
108
|
+
|
|
109
|
+
if (v9Value) {
|
|
110
|
+
return { name: 'V9_STACKCUSTOMDATA', value: v9Value }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: 'STACKCUSTOMDATA',
|
|
115
|
+
value: getField(fields, 'STACKCUSTOMDATA') || ''
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds one invalid stack custom data diagnostic.
|
|
121
|
+
* @param {{ sourceStream?: string }} record Source record.
|
|
122
|
+
* @param {string} fieldName Encoded field name.
|
|
123
|
+
* @returns {{ code: string, severity: string, message: string, sourceStream?: string, fieldName: string }}
|
|
124
|
+
*/
|
|
125
|
+
static #decodeDiagnostic(record, fieldName) {
|
|
126
|
+
return {
|
|
127
|
+
code: 'pcb.layer-stack-custom-data.decode-failed',
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
message:
|
|
130
|
+
'Compressed PCB layer stack custom data could not be decoded.',
|
|
131
|
+
sourceStream: record?.sourceStream,
|
|
132
|
+
fieldName
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Decodes one base64 zlib stack payload.
|
|
138
|
+
* @param {string} encoded Encoded field value.
|
|
139
|
+
* @returns {{ text: string, error: Error | null }}
|
|
140
|
+
*/
|
|
141
|
+
static #decodePayload(encoded) {
|
|
142
|
+
if (!encoded) {
|
|
143
|
+
return { text: '', error: null }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const inflated = unzlibSync(
|
|
148
|
+
PcbLayerStackCustomDataParser.#base64ToBytes(encoded)
|
|
149
|
+
)
|
|
150
|
+
const text = new TextDecoder()
|
|
151
|
+
.decode(inflated)
|
|
152
|
+
.replace(/^\?/, '')
|
|
153
|
+
.replace(/\0+$/u, '')
|
|
154
|
+
.trim()
|
|
155
|
+
|
|
156
|
+
return { text, error: null }
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return { text: '', error }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Converts base64 text into bytes in browser and Node runtimes.
|
|
164
|
+
* @param {string} encoded Base64 text.
|
|
165
|
+
* @returns {Uint8Array}
|
|
166
|
+
*/
|
|
167
|
+
static #base64ToBytes(encoded) {
|
|
168
|
+
const normalized = String(encoded || '').replace(/\s+/gu, '')
|
|
169
|
+
|
|
170
|
+
if (typeof Buffer !== 'undefined') {
|
|
171
|
+
return Uint8Array.from(Buffer.from(normalized, 'base64'))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const binary = globalThis.atob(normalized)
|
|
175
|
+
const bytes = new Uint8Array(binary.length)
|
|
176
|
+
|
|
177
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
178
|
+
bytes[index] = binary.charCodeAt(index) & 0xff
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return bytes
|
|
182
|
+
}
|
|
183
|
+
}
|