altium-toolkit 1.1.22 → 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 (99) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +134 -23
  3. package/docs/model-format.md +174 -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 +511 -1
  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 +2 -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/src/core/BinaryReader.mjs +213 -2
  35. package/src/core/altium/AltiumParser.mjs +352 -14
  36. package/src/core/altium/AltiumUnits.mjs +205 -0
  37. package/src/core/altium/AsciiRecordParser.mjs +9 -0
  38. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  39. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  40. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  41. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  42. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  43. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  44. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  45. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  46. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  47. package/src/core/altium/ParameterCollection.mjs +431 -0
  48. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  49. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  50. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  51. package/src/core/altium/ParserErrors.mjs +90 -0
  52. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  53. package/src/core/altium/ParserUtils.mjs +24 -0
  54. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  55. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  56. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  57. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  58. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  59. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  60. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  61. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  62. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  63. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  64. package/src/core/altium/PcbModelParser.mjs +223 -4
  65. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  66. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  67. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  68. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  69. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  70. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  71. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  72. package/src/core/altium/SchLibModelParser.mjs +840 -0
  73. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  74. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  75. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  76. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  77. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  78. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  79. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  80. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  81. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  82. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  83. package/src/core/altium/SchematicPinParser.mjs +12 -45
  84. package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
  85. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  86. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  87. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  88. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  89. package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
  90. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  91. package/src/core/altium/SchematicTextParser.mjs +28 -12
  92. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  93. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  94. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  95. package/src/parser.mjs +35 -1
  96. package/src/renderers.mjs +1 -0
  97. package/src/ui/SchematicShapeRenderer.mjs +49 -6
  98. package/src/ui/SchematicSvgRenderer.mjs +37 -8
  99. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,313 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbClassReportBuilder } from './PcbClassReportBuilder.mjs'
6
+ import { PcbNetMembershipReportBuilder } from './PcbNetMembershipReportBuilder.mjs'
7
+ import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
8
+ import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
9
+
10
+ /**
11
+ * Builds one deterministic inspection artifact for normalized PCB models.
12
+ */
13
+ export class PcbInspectionReportBuilder {
14
+ static SCHEMA = 'altium-toolkit.pcb.inspection.a1'
15
+
16
+ /**
17
+ * Builds a PCB inspection report from a parser root or normalized PCB model.
18
+ * @param {object} input Parser root, PCB model, or options object.
19
+ * @returns {object}
20
+ */
21
+ static build(input = {}) {
22
+ const pcb = PcbInspectionReportBuilder.#pcb(input)
23
+ const diagnostics = PcbInspectionReportBuilder.#diagnostics(input, pcb)
24
+ const statistics =
25
+ input.statistics ||
26
+ pcb.statistics ||
27
+ PcbStatisticsBuilder.build(pcb)
28
+ const routeAnalysis =
29
+ input.routeAnalysis ||
30
+ pcb.routeAnalysis ||
31
+ PcbRouteAnalysisBuilder.build(pcb)
32
+ const netMembership =
33
+ input.netMembership ||
34
+ pcb.netMembership ||
35
+ PcbNetMembershipReportBuilder.build(pcb)
36
+ const classes =
37
+ input.classReport ||
38
+ pcb.classReport ||
39
+ PcbClassReportBuilder.build(pcb)
40
+ const rules = PcbInspectionReportBuilder.#rules(pcb?.rules || [])
41
+ const primitives = PcbInspectionReportBuilder.#primitives(pcb)
42
+ const diagnosticSummary =
43
+ PcbInspectionReportBuilder.#diagnosticSummary(diagnostics)
44
+ const summary = PcbInspectionReportBuilder.#summary({
45
+ input,
46
+ statistics,
47
+ pcb,
48
+ primitives,
49
+ rules,
50
+ diagnosticSummary,
51
+ netMembership,
52
+ classes
53
+ })
54
+
55
+ return {
56
+ schema: PcbInspectionReportBuilder.SCHEMA,
57
+ units: statistics.units || {
58
+ coordinate: 'mil',
59
+ length: 'mil',
60
+ board: 'mil'
61
+ },
62
+ summary,
63
+ board: statistics.board || {},
64
+ statistics,
65
+ primitives,
66
+ rules,
67
+ diagnostics: {
68
+ summary: diagnosticSummary,
69
+ items: diagnostics.map((diagnostic) =>
70
+ PcbInspectionReportBuilder.#diagnosticRow(diagnostic)
71
+ )
72
+ },
73
+ netMembership,
74
+ classes,
75
+ routeAnalysis: {
76
+ schema: routeAnalysis.schema,
77
+ summary: routeAnalysis.summary || {}
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Resolves the PCB payload from a parser root or direct PCB model.
84
+ * @param {object} input Parser root or PCB model.
85
+ * @returns {object}
86
+ */
87
+ static #pcb(input) {
88
+ return input?.pcb || input || {}
89
+ }
90
+
91
+ /**
92
+ * Resolves diagnostics from the root or PCB object.
93
+ * @param {object} input Parser root or options object.
94
+ * @param {object} pcb Normalized PCB model.
95
+ * @returns {object[]}
96
+ */
97
+ static #diagnostics(input, pcb) {
98
+ if (Array.isArray(input?.diagnostics)) {
99
+ return input.diagnostics
100
+ }
101
+ if (Array.isArray(pcb?.diagnostics)) {
102
+ return pcb.diagnostics
103
+ }
104
+
105
+ return []
106
+ }
107
+
108
+ /**
109
+ * Builds primitive-family counters.
110
+ * @param {object} pcb Normalized PCB model.
111
+ * @returns {object}
112
+ */
113
+ static #primitives(pcb) {
114
+ const families = [
115
+ 'pads',
116
+ 'tracks',
117
+ 'arcs',
118
+ 'vias',
119
+ 'fills',
120
+ 'regions',
121
+ 'shapeBasedRegions',
122
+ 'polygons',
123
+ 'texts',
124
+ 'boardRegions'
125
+ ]
126
+ const counts = Object.fromEntries(
127
+ families.map((family) => [
128
+ family + 'Count',
129
+ Array.isArray(pcb?.[family]) ? pcb[family].length : 0
130
+ ])
131
+ )
132
+ const primitiveCount = [
133
+ 'padsCount',
134
+ 'tracksCount',
135
+ 'arcsCount',
136
+ 'viasCount',
137
+ 'fillsCount',
138
+ 'regionsCount',
139
+ 'shapeBasedRegionsCount',
140
+ 'polygonsCount'
141
+ ].reduce((total, key) => total + Number(counts[key] || 0), 0)
142
+
143
+ return {
144
+ ...counts,
145
+ componentCount: Array.isArray(pcb?.components)
146
+ ? pcb.components.length
147
+ : 0,
148
+ primitiveCount
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Builds design-rule counters by kind.
154
+ * @param {object[]} rules Design-rule rows.
155
+ * @returns {object}
156
+ */
157
+ static #rules(rules) {
158
+ const counts = new Map()
159
+
160
+ for (const rule of Array.isArray(rules) ? rules : []) {
161
+ const kind = PcbInspectionReportBuilder.#ruleKind(rule)
162
+ counts.set(kind, Number(counts.get(kind) || 0) + 1)
163
+ }
164
+
165
+ return {
166
+ count: Array.isArray(rules) ? rules.length : 0,
167
+ byKind: [...counts.entries()]
168
+ .map(([kind, count]) => ({ kind, count }))
169
+ .sort((left, right) =>
170
+ PcbInspectionReportBuilder.#naturalCompare(
171
+ left.kind,
172
+ right.kind
173
+ )
174
+ )
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Resolves one design-rule kind label.
180
+ * @param {object} rule Design-rule row.
181
+ * @returns {string}
182
+ */
183
+ static #ruleKind(rule) {
184
+ const kind = String(
185
+ rule?.kind ||
186
+ rule?.ruleKind ||
187
+ rule?.kindName ||
188
+ rule?.type ||
189
+ 'unknown'
190
+ ).trim()
191
+
192
+ return kind || 'unknown'
193
+ }
194
+
195
+ /**
196
+ * Builds diagnostic severity counters.
197
+ * @param {object[]} diagnostics Diagnostic rows.
198
+ * @returns {object}
199
+ */
200
+ static #diagnosticSummary(diagnostics) {
201
+ const rows = Array.isArray(diagnostics) ? diagnostics : []
202
+
203
+ return {
204
+ diagnosticCount: rows.length,
205
+ errorCount: rows.filter(
206
+ (diagnostic) =>
207
+ PcbInspectionReportBuilder.#severity(diagnostic) === 'error'
208
+ ).length,
209
+ warningCount: rows.filter(
210
+ (diagnostic) =>
211
+ PcbInspectionReportBuilder.#severity(diagnostic) ===
212
+ 'warning'
213
+ ).length,
214
+ infoCount: rows.filter(
215
+ (diagnostic) =>
216
+ PcbInspectionReportBuilder.#severity(diagnostic) === 'info'
217
+ ).length
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Builds one normalized diagnostic row.
223
+ * @param {object} diagnostic Diagnostic row.
224
+ * @returns {object}
225
+ */
226
+ static #diagnosticRow(diagnostic) {
227
+ return PcbInspectionReportBuilder.#stripEmpty({
228
+ severity: PcbInspectionReportBuilder.#severity(diagnostic),
229
+ code: diagnostic?.code,
230
+ message: diagnostic?.message,
231
+ source: diagnostic?.source,
232
+ errorKind: diagnostic?.errorKind
233
+ })
234
+ }
235
+
236
+ /**
237
+ * Resolves diagnostic severity.
238
+ * @param {object} diagnostic Diagnostic row.
239
+ * @returns {string}
240
+ */
241
+ static #severity(diagnostic) {
242
+ const severity = String(diagnostic?.severity || '')
243
+ .trim()
244
+ .toLowerCase()
245
+ if (['error', 'warning', 'info'].includes(severity)) {
246
+ return severity
247
+ }
248
+
249
+ return 'info'
250
+ }
251
+
252
+ /**
253
+ * Builds the top-level inspection summary.
254
+ * @param {object} parts Report parts.
255
+ * @returns {object}
256
+ */
257
+ static #summary(parts) {
258
+ const board = parts.statistics.board || {}
259
+ const reviewItemCount =
260
+ parts.diagnosticSummary.errorCount +
261
+ parts.diagnosticSummary.warningCount +
262
+ Number(parts.netMembership.summary.undeclaredNetCount || 0) +
263
+ Number(parts.netMembership.summary.unownedPrimitiveCount || 0) +
264
+ Number(parts.classes.summary.unresolvedMemberCount || 0)
265
+
266
+ return {
267
+ fileName: parts.input.fileName || '',
268
+ status: reviewItemCount > 0 ? 'needs-review' : 'clean',
269
+ boardWidthMil: board.widthMil || 0,
270
+ boardHeightMil: board.heightMil || 0,
271
+ layerCount: Array.isArray(parts.pcb?.layers)
272
+ ? parts.pcb.layers.length
273
+ : Number(parts.statistics.layers?.count || 0),
274
+ netCount: Array.isArray(parts.pcb?.nets)
275
+ ? parts.pcb.nets.length
276
+ : Number(parts.netMembership.summary.declaredNetCount || 0),
277
+ componentCount: parts.primitives.componentCount,
278
+ primitiveCount: parts.primitives.primitiveCount,
279
+ ruleCount: parts.rules.count,
280
+ diagnosticCount: parts.diagnosticSummary.diagnosticCount,
281
+ errorCount: parts.diagnosticSummary.errorCount,
282
+ warningCount: parts.diagnosticSummary.warningCount,
283
+ possibleUnroutedNetCount:
284
+ parts.netMembership.summary.possibleUnroutedNetCount || 0,
285
+ classIssueCount: parts.classes.summary.issueCount || 0
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Sorts strings with numeric chunks in human order.
291
+ * @param {string} left Left value.
292
+ * @param {string} right Right value.
293
+ * @returns {number}
294
+ */
295
+ static #naturalCompare(left, right) {
296
+ return String(left).localeCompare(String(right), undefined, {
297
+ numeric: true
298
+ })
299
+ }
300
+
301
+ /**
302
+ * Removes undefined and blank-string values from a shallow object.
303
+ * @param {Record<string, unknown>} row Input row.
304
+ * @returns {Record<string, unknown>}
305
+ */
306
+ static #stripEmpty(row) {
307
+ return Object.fromEntries(
308
+ Object.entries(row).filter(
309
+ ([, value]) => value !== undefined && value !== ''
310
+ )
311
+ )
312
+ }
313
+ }
@@ -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
+ }