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.
Files changed (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +7 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. 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
+ }