altium-toolkit 1.0.7 → 1.0.9

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 (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,245 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds a read-only PCB primitive ownership graph from normalized indexes.
7
+ */
8
+ export class PcbOwnershipGraphBuilder {
9
+ static SCHEMA_ID = 'altium-toolkit.pcb.ownership.a1'
10
+
11
+ /**
12
+ * Builds primitive owner groups keyed by component, net, and polygon index.
13
+ * @param {{ components?: object[], nets?: object[], fills?: object[], tracks?: object[], arcs?: object[], vias?: object[], pads?: object[], regions?: object[], shapeBasedRegions?: object[], texts?: object[] }} pcb Normalized PCB model.
14
+ * @returns {{ schema: string, primitiveOwners: object[], componentsByIndex: Record<string, object>, netsByIndex: Record<string, object>, polygonsByIndex: Record<string, object> }}
15
+ */
16
+ static build(pcb) {
17
+ const componentNames = PcbOwnershipGraphBuilder.#componentNames(
18
+ pcb?.components || []
19
+ )
20
+ const netNames = PcbOwnershipGraphBuilder.#netNames(pcb?.nets || [])
21
+ const componentsByIndex =
22
+ PcbOwnershipGraphBuilder.#initialComponentGroups(
23
+ pcb?.components || []
24
+ )
25
+ const netsByIndex = {}
26
+ const polygonsByIndex = {}
27
+ const primitiveOwners = []
28
+
29
+ for (const item of PcbOwnershipGraphBuilder.#primitiveItems(pcb)) {
30
+ const owner = PcbOwnershipGraphBuilder.#primitiveOwner(
31
+ item,
32
+ componentNames,
33
+ netNames
34
+ )
35
+ if (!owner) {
36
+ continue
37
+ }
38
+
39
+ primitiveOwners.push(owner)
40
+ PcbOwnershipGraphBuilder.#addGroupKey(
41
+ componentsByIndex,
42
+ owner.componentIndex,
43
+ {
44
+ componentIndex: owner.componentIndex,
45
+ designator: owner.component || '',
46
+ primitiveKeys: []
47
+ },
48
+ owner.primitiveKey
49
+ )
50
+ PcbOwnershipGraphBuilder.#addGroupKey(
51
+ netsByIndex,
52
+ owner.netIndex,
53
+ {
54
+ netIndex: owner.netIndex,
55
+ name: owner.net || '',
56
+ primitiveKeys: []
57
+ },
58
+ owner.primitiveKey
59
+ )
60
+ PcbOwnershipGraphBuilder.#addGroupKey(
61
+ polygonsByIndex,
62
+ owner.polygonIndex,
63
+ {
64
+ polygonIndex: owner.polygonIndex,
65
+ primitiveKeys: []
66
+ },
67
+ owner.primitiveKey
68
+ )
69
+ }
70
+
71
+ return {
72
+ schema: PcbOwnershipGraphBuilder.SCHEMA_ID,
73
+ primitiveOwners,
74
+ componentsByIndex,
75
+ netsByIndex,
76
+ polygonsByIndex
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Builds primitive iterable entries in stable renderer collection order.
82
+ * @param {object} pcb Normalized PCB model.
83
+ * @returns {{ primitiveKind: string, primitiveKey: string, primitive: object }[]}
84
+ */
85
+ static #primitiveItems(pcb) {
86
+ return [
87
+ ['fill', pcb?.fills || []],
88
+ ['track', pcb?.tracks || []],
89
+ ['arc', pcb?.arcs || []],
90
+ ['via', pcb?.vias || []],
91
+ ['pad', pcb?.pads || []],
92
+ ['region', pcb?.regions || []],
93
+ ['shape-based-region', pcb?.shapeBasedRegions || []],
94
+ ['text', pcb?.texts || []],
95
+ ['polygon', pcb?.polygons || []]
96
+ ].flatMap(([primitiveKind, primitives]) =>
97
+ primitives.map((primitive, index) => ({
98
+ primitiveKind,
99
+ primitiveKey: primitiveKind + '-' + index,
100
+ primitive
101
+ }))
102
+ )
103
+ }
104
+
105
+ /**
106
+ * Builds a compact primitive owner row.
107
+ * @param {{ primitiveKind: string, primitiveKey: string, primitive: object }} item Primitive item.
108
+ * @param {Map<number, string>} componentNames Component names by native index.
109
+ * @param {Map<number, string>} netNames Net names by native index.
110
+ * @returns {object | null}
111
+ */
112
+ static #primitiveOwner(item, componentNames, netNames) {
113
+ const componentIndex =
114
+ PcbOwnershipGraphBuilder.#optionalInteger(
115
+ item.primitive.componentIndex
116
+ ) ??
117
+ (item.primitiveKind === 'text'
118
+ ? PcbOwnershipGraphBuilder.#optionalInteger(
119
+ item.primitive.ownerIndex
120
+ )
121
+ : null)
122
+ const netIndex = PcbOwnershipGraphBuilder.#optionalInteger(
123
+ item.primitive.netIndex
124
+ )
125
+ const polygonIndex = PcbOwnershipGraphBuilder.#optionalInteger(
126
+ item.primitive.polygonIndex
127
+ )
128
+
129
+ if (
130
+ componentIndex === null &&
131
+ netIndex === null &&
132
+ polygonIndex === null
133
+ ) {
134
+ return null
135
+ }
136
+
137
+ return {
138
+ primitiveKey: item.primitiveKey,
139
+ primitiveKind: item.primitiveKind,
140
+ componentIndex,
141
+ component:
142
+ componentIndex === null
143
+ ? ''
144
+ : componentNames.get(componentIndex) || '',
145
+ netIndex,
146
+ net: netIndex === null ? '' : netNames.get(netIndex) || '',
147
+ polygonIndex
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Builds component designator lookup by native component index.
153
+ * @param {object[]} components Component rows.
154
+ * @returns {Map<number, string>}
155
+ */
156
+ static #componentNames(components) {
157
+ const names = new Map()
158
+
159
+ for (const component of components || []) {
160
+ const componentIndex = PcbOwnershipGraphBuilder.#optionalInteger(
161
+ component?.componentIndex
162
+ )
163
+ if (componentIndex !== null) {
164
+ names.set(componentIndex, String(component.designator || ''))
165
+ }
166
+ }
167
+
168
+ return names
169
+ }
170
+
171
+ /**
172
+ * Builds net name lookup by native net index.
173
+ * @param {object[]} nets Net rows.
174
+ * @returns {Map<number, string>}
175
+ */
176
+ static #netNames(nets) {
177
+ const names = new Map()
178
+
179
+ for (const net of nets || []) {
180
+ const netIndex = PcbOwnershipGraphBuilder.#optionalInteger(
181
+ net?.netIndex
182
+ )
183
+ if (netIndex !== null) {
184
+ names.set(netIndex, String(net.name || ''))
185
+ }
186
+ }
187
+
188
+ return names
189
+ }
190
+
191
+ /**
192
+ * Creates empty component groups so consumers can inspect ownerless rows.
193
+ * @param {object[]} components Component rows.
194
+ * @returns {Record<string, object>}
195
+ */
196
+ static #initialComponentGroups(components) {
197
+ const groups = {}
198
+
199
+ for (const component of components || []) {
200
+ const componentIndex = PcbOwnershipGraphBuilder.#optionalInteger(
201
+ component?.componentIndex
202
+ )
203
+ if (componentIndex !== null) {
204
+ groups[String(componentIndex)] = {
205
+ componentIndex,
206
+ designator: String(component.designator || ''),
207
+ primitiveKeys: []
208
+ }
209
+ }
210
+ }
211
+
212
+ return groups
213
+ }
214
+
215
+ /**
216
+ * Adds one primitive key to a numeric owner group.
217
+ * @param {Record<string, object>} groups Group map.
218
+ * @param {number | null} index Owner index.
219
+ * @param {object} fallbackGroup Group to create when missing.
220
+ * @param {string} primitiveKey Primitive key to append.
221
+ */
222
+ static #addGroupKey(groups, index, fallbackGroup, primitiveKey) {
223
+ if (index === null) {
224
+ return
225
+ }
226
+
227
+ const key = String(index)
228
+ if (!groups[key]) {
229
+ groups[key] = fallbackGroup
230
+ }
231
+ if (!groups[key].primitiveKeys.includes(primitiveKey)) {
232
+ groups[key].primitiveKeys.push(primitiveKey)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Parses an optional integer value.
238
+ * @param {unknown} value Candidate value.
239
+ * @returns {number | null}
240
+ */
241
+ static #optionalInteger(value) {
242
+ const parsed = Number(value)
243
+ return Number.isInteger(parsed) ? parsed : null
244
+ }
245
+ }
@@ -35,19 +35,18 @@ export class PcbPadPrimitiveParser {
35
35
  return []
36
36
  }
37
37
 
38
- let offset = 0
38
+ const records = PcbPadPrimitiveParser.#readPadRecordSequence(
39
+ normalizedData,
40
+ 0,
41
+ count
42
+ )
39
43
  const pads = []
40
44
 
41
- for (let index = 0; index < count; index += 1) {
42
- const record = PcbPadPrimitiveParser.#readPadRecordAt(
43
- normalizedData,
44
- offset
45
- )
46
-
47
- if (!record) {
48
- return []
49
- }
45
+ if (!records) {
46
+ return []
47
+ }
50
48
 
49
+ for (const record of records) {
51
50
  const pad = PcbPadPrimitiveParser.#parsePadSubrecords(
52
51
  record.subrecords
53
52
  )
@@ -57,25 +56,69 @@ export class PcbPadPrimitiveParser {
57
56
  }
58
57
 
59
58
  pads.push(pad)
60
- offset = record.nextOffset
61
-
62
- if (index < count - 1) {
63
- const nextOffset =
64
- PcbPadPrimitiveParser.#findNextPadRecordOffset(
65
- normalizedData,
66
- offset,
67
- count - index - 1
68
- )
69
-
70
- if (nextOffset === null) {
71
- return []
59
+ }
60
+
61
+ return pads
62
+ }
63
+
64
+ /**
65
+ * Reads all expected pad records without recursive suffix validation.
66
+ * @param {Uint8Array} bytes
67
+ * @param {number} offset
68
+ * @param {number} count
69
+ * @returns {{ subrecords: DataView[], nextOffset: number }[] | null}
70
+ */
71
+ static #readPadRecordSequence(bytes, offset, count) {
72
+ const firstRecord = PcbPadPrimitiveParser.#readPadRecordAt(
73
+ bytes,
74
+ offset
75
+ )
76
+
77
+ if (!firstRecord) {
78
+ return null
79
+ }
80
+
81
+ const records = [firstRecord]
82
+ const alternativeScanOffsets = [null]
83
+ let depth = 1
84
+ let scanOffset = firstRecord.nextOffset
85
+
86
+ while (depth < count) {
87
+ const candidate = PcbPadPrimitiveParser.#findNextPadRecordCandidate(
88
+ bytes,
89
+ scanOffset
90
+ )
91
+
92
+ if (!candidate) {
93
+ let foundAlternative = false
94
+
95
+ while (depth > 1 && !foundAlternative) {
96
+ depth -= 1
97
+ records.length = depth
98
+
99
+ const alternativeOffset = alternativeScanOffsets[depth]
100
+ alternativeScanOffsets.length = depth
101
+
102
+ if (alternativeOffset !== null) {
103
+ scanOffset = alternativeOffset
104
+ foundAlternative = true
105
+ }
72
106
  }
73
107
 
74
- offset = nextOffset
108
+ if (!foundAlternative) {
109
+ return null
110
+ }
111
+
112
+ continue
75
113
  }
114
+
115
+ records[depth] = candidate.record
116
+ alternativeScanOffsets[depth] = candidate.alternativeOffset
117
+ depth += 1
118
+ scanOffset = candidate.record.nextOffset
76
119
  }
77
120
 
78
- return pads
121
+ return records
79
122
  }
80
123
 
81
124
  /**
@@ -149,31 +192,28 @@ export class PcbPadPrimitiveParser {
149
192
  }
150
193
 
151
194
  /**
152
- * Finds the next pad record boundary after optional unknown subrecords.
195
+ * Finds the next readable pad record after optional unknown subrecords.
153
196
  * @param {Uint8Array} bytes
154
197
  * @param {number} offset
155
- * @param {number} remainingCount
156
- * @returns {number | null}
198
+ * @returns {{ record: { subrecords: DataView[], nextOffset: number }, alternativeOffset: number | null } | null}
157
199
  */
158
- static #findNextPadRecordOffset(bytes, offset, remainingCount) {
200
+ static #findNextPadRecordCandidate(bytes, offset) {
159
201
  let cursor = offset
160
202
 
161
203
  while (cursor < bytes.byteLength) {
162
- if (
163
- PcbPadPrimitiveParser.#canReadPadRecordSequence(
164
- bytes,
165
- cursor,
166
- remainingCount
167
- )
168
- ) {
169
- return cursor
170
- }
171
-
204
+ const record = PcbPadPrimitiveParser.#readPadRecordAt(bytes, cursor)
172
205
  const unknownSubrecord = PcbPadPrimitiveParser.#readSubrecordAt(
173
206
  bytes,
174
207
  cursor
175
208
  )
176
209
 
210
+ if (record) {
211
+ return {
212
+ record,
213
+ alternativeOffset: unknownSubrecord?.nextOffset ?? null
214
+ }
215
+ }
216
+
177
217
  if (!unknownSubrecord) {
178
218
  return null
179
219
  }
@@ -184,33 +224,6 @@ export class PcbPadPrimitiveParser {
184
224
  return null
185
225
  }
186
226
 
187
- /**
188
- * Checks whether the remaining pad records can be read from an offset.
189
- * @param {Uint8Array} bytes
190
- * @param {number} offset
191
- * @param {number} remainingCount
192
- * @returns {boolean}
193
- */
194
- static #canReadPadRecordSequence(bytes, offset, remainingCount) {
195
- const record = PcbPadPrimitiveParser.#readPadRecordAt(bytes, offset)
196
-
197
- if (!record) {
198
- return false
199
- }
200
-
201
- if (remainingCount <= 1) {
202
- return true
203
- }
204
-
205
- return (
206
- PcbPadPrimitiveParser.#findNextPadRecordOffset(
207
- bytes,
208
- record.nextOffset,
209
- remainingCount - 1
210
- ) !== null
211
- )
212
- }
213
-
214
227
  /**
215
228
  * Decodes one pad payload from its subrecords.
216
229
  * @param {DataView[]} subrecords
@@ -48,6 +48,12 @@ export class PcbPadStackParser {
48
48
 
49
49
  static #SOLDER_MASK_CACHE_VALID_OFFSET = 104
50
50
 
51
+ static #POSITIVE_TOLERANCE_OFFSET = 162
52
+
53
+ static #NEGATIVE_TOLERANCE_OFFSET = 166
54
+
55
+ static #HOLE_TOLERANCE_UNSET = 0x7fffffff
56
+
51
57
  static #EXTENSION_MIN_BYTE_LENGTH = 596
52
58
 
53
59
  static #INNER_LAYER_COUNT = 29
@@ -178,6 +184,7 @@ export class PcbPadStackParser {
178
184
 
179
185
  PcbPadStackParser.#assignPadCacheFields(result, mainRecord)
180
186
  PcbPadStackParser.#assignMaskCacheFields(result, mainRecord)
187
+ PcbPadStackParser.#assignHoleToleranceFields(result, mainRecord)
181
188
 
182
189
  return result
183
190
  }
@@ -292,6 +299,35 @@ export class PcbPadStackParser {
292
299
  }
293
300
  }
294
301
 
302
+ /**
303
+ * Adds optional hole tolerance fields to an output object.
304
+ * @param {Record<string, unknown>} result
305
+ * @param {DataView} mainRecord
306
+ */
307
+ static #assignHoleToleranceFields(result, mainRecord) {
308
+ const positiveTolerance = PcbPadStackParser.#readHoleTolerance(
309
+ mainRecord,
310
+ PcbPadStackParser.#POSITIVE_TOLERANCE_OFFSET
311
+ )
312
+ const negativeTolerance = PcbPadStackParser.#readHoleTolerance(
313
+ mainRecord,
314
+ PcbPadStackParser.#NEGATIVE_TOLERANCE_OFFSET
315
+ )
316
+ const holeTolerance = {}
317
+
318
+ if (positiveTolerance !== null) {
319
+ result.positiveTolerance = positiveTolerance
320
+ holeTolerance.positive = positiveTolerance
321
+ }
322
+ if (negativeTolerance !== null) {
323
+ result.negativeTolerance = negativeTolerance
324
+ holeTolerance.negative = negativeTolerance
325
+ }
326
+ if (Object.keys(holeTolerance).length) {
327
+ result.holeTolerance = holeTolerance
328
+ }
329
+ }
330
+
295
331
  /**
296
332
  * Returns whether one decoded pad cache contains meaningful data.
297
333
  * @param {{ planeConnectionStyle: number, thermalReliefConductorWidth: number, thermalReliefConductorCount: number, thermalReliefAirGap: number, powerPlaneReliefExpansion: number, powerPlaneClearance: number, validity: Record<string, number> }} padCache
@@ -311,6 +347,28 @@ export class PcbPadStackParser {
311
347
  return values.some((value) => value !== 0)
312
348
  }
313
349
 
350
+ /**
351
+ * Reads one optional hole tolerance from a pad main record.
352
+ * @param {DataView} mainRecord
353
+ * @param {number} offset
354
+ * @returns {number | null}
355
+ */
356
+ static #readHoleTolerance(mainRecord, offset) {
357
+ if (!mainRecord || offset + 4 > mainRecord.byteLength) {
358
+ return null
359
+ }
360
+
361
+ const rawValue = mainRecord.getInt32(offset, true)
362
+ if (
363
+ rawValue === 0 ||
364
+ rawValue === PcbPadStackParser.#HOLE_TOLERANCE_UNSET
365
+ ) {
366
+ return null
367
+ }
368
+
369
+ return rawValue / 10000
370
+ }
371
+
314
372
  /**
315
373
  * Adds derived mask-expansion and layer-opening semantics.
316
374
  * @param {Record<string, boolean | number>} flags