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
@@ -53,6 +53,46 @@ const RULE_TYPES = new Map([
53
53
  displayName: 'Routing Layers'
54
54
  }
55
55
  ],
56
+ [
57
+ 'ROUTINGTOPOLOGY',
58
+ {
59
+ kind: 'routing-topology',
60
+ category: 'routing',
61
+ displayName: 'Routing Topology'
62
+ }
63
+ ],
64
+ [
65
+ 'ROUTINGCORNERS',
66
+ {
67
+ kind: 'routing-corners',
68
+ category: 'routing',
69
+ displayName: 'Routing Corners'
70
+ }
71
+ ],
72
+ [
73
+ 'ROUTINGCORNERSTYLE',
74
+ {
75
+ kind: 'routing-corners',
76
+ category: 'routing',
77
+ displayName: 'Routing Corners'
78
+ }
79
+ ],
80
+ [
81
+ 'ROUTINGPRIORITY',
82
+ {
83
+ kind: 'routing-priority',
84
+ category: 'routing',
85
+ displayName: 'Routing Priority'
86
+ }
87
+ ],
88
+ [
89
+ 'FANOUTCONTROL',
90
+ {
91
+ kind: 'fanout-control',
92
+ category: 'routing',
93
+ displayName: 'Fanout Control'
94
+ }
95
+ ],
56
96
  [
57
97
  'DIFFERENTIALPAIRROUTING',
58
98
  {
@@ -84,6 +124,315 @@ const RULE_TYPES = new Map([
84
124
  category: 'manufacturing',
85
125
  displayName: 'Silkscreen Over Component Pads'
86
126
  }
127
+ ],
128
+ [
129
+ 'MINIMUMSOLDERMASKSLIVER',
130
+ {
131
+ kind: 'minimum-soldermask-sliver',
132
+ category: 'manufacturing',
133
+ displayName: 'Minimum Solder Mask Sliver'
134
+ }
135
+ ],
136
+ [
137
+ 'SILKTOSILKCLEARANCE',
138
+ {
139
+ kind: 'silk-to-silk-clearance',
140
+ category: 'manufacturing',
141
+ displayName: 'Silkscreen To Silkscreen Clearance'
142
+ }
143
+ ],
144
+ [
145
+ 'SILKTOSOLDERMASKCLEARANCE',
146
+ {
147
+ kind: 'silk-to-soldermask-clearance',
148
+ category: 'manufacturing',
149
+ displayName: 'Silkscreen To Solder Mask Clearance'
150
+ }
151
+ ],
152
+ [
153
+ 'COMPONENTCLEARANCE',
154
+ {
155
+ kind: 'component-clearance',
156
+ category: 'placement',
157
+ displayName: 'Component Clearance'
158
+ }
159
+ ],
160
+ ['LENGTH', { kind: 'length', category: 'routing', displayName: 'Length' }],
161
+ [
162
+ 'MAXMINLENGTH',
163
+ { kind: 'length', category: 'routing', displayName: 'Length' }
164
+ ],
165
+ [
166
+ 'MATCHEDLENGTHS',
167
+ {
168
+ kind: 'matched-lengths',
169
+ category: 'routing',
170
+ displayName: 'Matched Lengths'
171
+ }
172
+ ],
173
+ [
174
+ 'MATCHEDNLENGTH',
175
+ {
176
+ kind: 'matched-lengths',
177
+ category: 'routing',
178
+ displayName: 'Matched Lengths'
179
+ }
180
+ ],
181
+ [
182
+ 'MINIMUMANNULARRING',
183
+ {
184
+ kind: 'minimum-annular-ring',
185
+ category: 'manufacturing',
186
+ displayName: 'Minimum Annular Ring'
187
+ }
188
+ ],
189
+ [
190
+ 'VIASUNDERSMD',
191
+ {
192
+ kind: 'vias-under-smd',
193
+ category: 'manufacturing',
194
+ displayName: 'Vias Under SMD'
195
+ }
196
+ ],
197
+ [
198
+ 'ASSEMBLYTESTPOINT',
199
+ {
200
+ kind: 'assembly-testpoint',
201
+ category: 'testpoint',
202
+ displayName: 'Assembly Testpoint'
203
+ }
204
+ ],
205
+ [
206
+ 'ASSYTESTPOINTSTYLE',
207
+ {
208
+ kind: 'assembly-testpoint',
209
+ category: 'testpoint',
210
+ displayName: 'Assembly Testpoint'
211
+ }
212
+ ],
213
+ [
214
+ 'FABRICATIONTESTPOINT',
215
+ {
216
+ kind: 'fabrication-testpoint',
217
+ category: 'testpoint',
218
+ displayName: 'Fabrication Testpoint'
219
+ }
220
+ ],
221
+ [
222
+ 'TESTPOINTSTYLE',
223
+ {
224
+ kind: 'fabrication-testpoint',
225
+ category: 'testpoint',
226
+ displayName: 'Fabrication Testpoint'
227
+ }
228
+ ],
229
+ [
230
+ 'TESTPOINT',
231
+ {
232
+ kind: 'testpoint',
233
+ category: 'testpoint',
234
+ displayName: 'Testpoint'
235
+ }
236
+ ],
237
+ [
238
+ 'TESTPOINTUSAGE',
239
+ {
240
+ kind: 'testpoint-usage',
241
+ category: 'testpoint',
242
+ displayName: 'Testpoint Usage'
243
+ }
244
+ ],
245
+ [
246
+ 'FABRICATIONTESTPOINTUSAGE',
247
+ {
248
+ kind: 'testpoint-usage',
249
+ category: 'testpoint',
250
+ displayName: 'Testpoint Usage'
251
+ }
252
+ ],
253
+ [
254
+ 'ASSYTESTPOINTUSAGE',
255
+ {
256
+ kind: 'assembly-testpoint-usage',
257
+ category: 'testpoint',
258
+ displayName: 'Assembly Testpoint Usage'
259
+ }
260
+ ]
261
+ ])
262
+
263
+ const TYPED_CONSTRAINT_ALIASES = new Map([
264
+ [
265
+ 'width',
266
+ {
267
+ minWidth: ['MINLIMIT', 'MINWIDTH'],
268
+ preferredWidth: ['PREFEREDWIDTH', 'PREFERREDWIDTH'],
269
+ maxWidth: ['MAXLIMIT', 'MAXWIDTH']
270
+ }
271
+ ],
272
+ [
273
+ 'clearance',
274
+ {
275
+ minClearance: ['GAP', 'MINDISTANCE', 'CLEARANCE'],
276
+ genericClearance: ['GENERICCLEARANCE']
277
+ }
278
+ ],
279
+ [
280
+ 'fanout-control',
281
+ {
282
+ fanoutStyle: ['FANOUTSTYLE'],
283
+ fanoutDirection: ['FANOUTDIRECTION'],
284
+ bgaDirection: ['BGADIR'],
285
+ bgaViaMode: ['BGAVIAMODE'],
286
+ viaGrid: ['VIAGRID']
287
+ }
288
+ ],
289
+ [
290
+ 'routing-topology',
291
+ {
292
+ topology: ['TOPOLOGY']
293
+ }
294
+ ],
295
+ [
296
+ 'routing-corners',
297
+ {
298
+ cornerStyle: ['CORNERSTYLE'],
299
+ minimumSetback: ['MINSETBACK'],
300
+ maximumSetback: ['MAXSETBACK']
301
+ }
302
+ ],
303
+ [
304
+ 'routing-priority',
305
+ {
306
+ routingPriority: ['ROUTINGPRIORITY']
307
+ }
308
+ ],
309
+ [
310
+ 'length',
311
+ {
312
+ minimumLength: ['MINLIMIT'],
313
+ maximumLength: ['MAXLIMIT'],
314
+ minimumDelay: ['MINDELAY'],
315
+ maximumDelay: ['MAXDELAY'],
316
+ useDelayUnits: ['USEDELAYUNITS']
317
+ }
318
+ ],
319
+ [
320
+ 'matched-lengths',
321
+ {
322
+ tolerance: ['TOLERANCE'],
323
+ delayTolerance: ['DELAYTOLERANCE'],
324
+ targetSourceName: ['TARGETSOURCENAME'],
325
+ useDelayUnits: ['USEDELAYUNITS'],
326
+ checkNetsInDiffPair: ['CHECKNETSINDIFFPAIR'],
327
+ checkDiffPairVsDiffPair: ['CHECKDIFFPAIRVSDIFFPAIR'],
328
+ checkOthers: ['CHECKOTHERS'],
329
+ checkXSignals: ['CHECKXSIGNALS']
330
+ }
331
+ ],
332
+ [
333
+ 'minimum-soldermask-sliver',
334
+ {
335
+ minimumSolderMaskWidth: ['MINSOLDERMASKWIDTH']
336
+ }
337
+ ],
338
+ [
339
+ 'silk-to-silk-clearance',
340
+ {
341
+ silkToSilkClearance: ['SILKTOSILKCLEARANCE']
342
+ }
343
+ ],
344
+ [
345
+ 'silk-to-soldermask-clearance',
346
+ {
347
+ minimumSilkscreenToMaskGap: ['MINSILKSCREENTOMASKGAP'],
348
+ clearanceToExposedCopper: ['CLEARANCETOEXPOSEDCOPPER']
349
+ }
350
+ ],
351
+ [
352
+ 'silkscreen-over-component-pads',
353
+ {
354
+ minimumSilkscreenToMaskGap: ['MINSILKSCREENTOMASKGAP']
355
+ }
356
+ ],
357
+ [
358
+ 'component-clearance',
359
+ {
360
+ gap: ['GAP'],
361
+ verticalGap: ['VERTICALGAP'],
362
+ collisionCheckMode: ['COLLISIONCHECKMODE'],
363
+ showDistances: ['SHOWDISTANCES'],
364
+ doNotCheckWithout3dBody: ['DONOTCHECKWITHOUT3DBODY']
365
+ }
366
+ ],
367
+ [
368
+ 'minimum-annular-ring',
369
+ {
370
+ minimumRing: ['MINIMUMRING']
371
+ }
372
+ ],
373
+ [
374
+ 'vias-under-smd',
375
+ {
376
+ allowed: ['ALLOWED']
377
+ }
378
+ ],
379
+ [
380
+ 'assembly-testpoint',
381
+ {
382
+ minimumSize: ['MINSIZE'],
383
+ preferredSize: ['PREFEREDSIZE', 'PREFERREDSIZE'],
384
+ maximumSize: ['MAXSIZE'],
385
+ minimumHoleSize: ['MINHOLESIZE'],
386
+ preferredHoleSize: ['PREFEREDHOLESIZE', 'PREFERREDHOLESIZE'],
387
+ maximumHoleSize: ['MAXHOLESIZE'],
388
+ testpointGrid: ['TESTPOINTGRID'],
389
+ gridTolerance: ['GRIDTOLERANCE'],
390
+ useGrid: ['USEGRID'],
391
+ testpointUnderComponent: ['TESTPOINTUNDERCOMPONENT'],
392
+ allowSideTop: ['ALLOWSIDETOP'],
393
+ allowSideBottom: ['ALLOWSIDEBOTTOM']
394
+ }
395
+ ],
396
+ [
397
+ 'fabrication-testpoint',
398
+ {
399
+ minimumSize: ['MINSIZE'],
400
+ preferredSize: ['PREFEREDSIZE', 'PREFERREDSIZE'],
401
+ maximumSize: ['MAXSIZE'],
402
+ minimumHoleSize: ['MINHOLESIZE'],
403
+ preferredHoleSize: ['PREFEREDHOLESIZE', 'PREFERREDHOLESIZE'],
404
+ maximumHoleSize: ['MAXHOLESIZE'],
405
+ side: ['SIDE'],
406
+ testpointGrid: ['TESTPOINTGRID'],
407
+ gridTolerance: ['GRIDTOLERANCE'],
408
+ useGrid: ['USEGRID'],
409
+ testpointUnderComponent: ['TESTPOINTUNDERCOMPONENT'],
410
+ allowSideTop: ['ALLOWSIDETOP'],
411
+ allowSideBottom: ['ALLOWSIDEBOTTOM']
412
+ }
413
+ ],
414
+ [
415
+ 'testpoint',
416
+ {
417
+ minimumSize: ['MINSIZE'],
418
+ preferredSize: ['PREFEREDSIZE', 'PREFERREDSIZE'],
419
+ maximumSize: ['MAXSIZE'],
420
+ minimumHoleSize: ['MINHOLESIZE'],
421
+ preferredHoleSize: ['PREFEREDHOLESIZE', 'PREFERREDHOLESIZE'],
422
+ maximumHoleSize: ['MAXHOLESIZE'],
423
+ side: ['SIDE'],
424
+ style: ['STYLE'],
425
+ order: ['ORDER'],
426
+ testpointGrid: ['TESTPOINTGRID'],
427
+ testpointUnderComponent: ['TESTPOINTUNDERCOMPONENT']
428
+ }
429
+ ],
430
+ [
431
+ 'testpoint-usage',
432
+ {
433
+ allowMultiple: ['ALLOWMULTIPLE'],
434
+ valid: ['VALID']
435
+ }
87
436
  ]
88
437
  ])
89
438
 
@@ -296,7 +645,7 @@ export class PcbRuleParser {
296
645
  if (numeric) {
297
646
  return numeric
298
647
  }
299
- if (/^(TRUE|FALSE|T|F)$/iu.test(text)) {
648
+ if (/^(TRUE|FALSE|T|F|YES|NO|ON|OFF)$/iu.test(text)) {
300
649
  return {
301
650
  raw: text,
302
651
  type: 'boolean',
@@ -488,39 +837,11 @@ export class PcbRuleParser {
488
837
  * @returns {Record<string, Record<string, unknown>>}
489
838
  */
490
839
  static #parseTypedConstraints(ruleType, constraintValues) {
491
- if (ruleType.kind === 'width') {
492
- return PcbRuleParser.#parseWidthConstraints(constraintValues)
493
- }
494
- if (ruleType.kind === 'clearance') {
495
- return PcbRuleParser.#parseClearanceConstraints(constraintValues)
496
- }
497
-
498
- return {}
499
- }
840
+ const aliases = TYPED_CONSTRAINT_ALIASES.get(ruleType.kind)
500
841
 
501
- /**
502
- * Builds semantic aliases for width-rule constraints.
503
- * @param {Record<string, Record<string, unknown>>} constraintValues
504
- * @returns {Record<string, Record<string, unknown>>}
505
- */
506
- static #parseWidthConstraints(constraintValues) {
507
- return PcbRuleParser.#pickTypedConstraints(constraintValues, {
508
- minWidth: ['MINLIMIT', 'MINWIDTH'],
509
- preferredWidth: ['PREFEREDWIDTH', 'PREFERREDWIDTH'],
510
- maxWidth: ['MAXLIMIT', 'MAXWIDTH']
511
- })
512
- }
513
-
514
- /**
515
- * Builds semantic aliases for clearance-rule constraints.
516
- * @param {Record<string, Record<string, unknown>>} constraintValues
517
- * @returns {Record<string, Record<string, unknown>>}
518
- */
519
- static #parseClearanceConstraints(constraintValues) {
520
- return PcbRuleParser.#pickTypedConstraints(constraintValues, {
521
- minClearance: ['GAP', 'MINDISTANCE', 'CLEARANCE'],
522
- genericClearance: ['GENERICCLEARANCE']
523
- })
842
+ return aliases
843
+ ? PcbRuleParser.#pickTypedConstraints(constraintValues, aliases)
844
+ : {}
524
845
  }
525
846
 
526
847
  /**
@@ -0,0 +1,177 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
6
+
7
+ /**
8
+ * Decodes length-prefixed pipe-delimited PCB sidecar records.
9
+ */
10
+ export class PcbSidecarRecordParser {
11
+ /**
12
+ * Parses one sidecar stream into field records.
13
+ * @param {Uint8Array | ArrayBuffer | undefined} dataBytes
14
+ * @param {string} sourceStream
15
+ * @returns {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }[]}
16
+ */
17
+ static parseLengthPrefixedRecords(dataBytes, sourceStream) {
18
+ const bytes = PcbSidecarRecordParser.toUint8Array(dataBytes)
19
+ const records = []
20
+ let offset = 0
21
+
22
+ while (offset + 4 <= bytes.byteLength) {
23
+ const recordLength = PcbSidecarRecordParser.readUint32(
24
+ bytes,
25
+ offset
26
+ )
27
+ offset += 4
28
+
29
+ if (recordLength <= 0 || offset + recordLength > bytes.byteLength) {
30
+ break
31
+ }
32
+
33
+ const recordBytes = bytes.subarray(offset, offset + recordLength)
34
+ offset += recordLength
35
+
36
+ records.push({
37
+ fields: PcbSidecarRecordParser.parseRecordFields(recordBytes),
38
+ sourceStream,
39
+ recordIndex: records.length
40
+ })
41
+ }
42
+
43
+ return records
44
+ }
45
+
46
+ /**
47
+ * Parses one pipe-delimited field record.
48
+ * @param {Uint8Array} bytes
49
+ * @returns {Record<string, string>}
50
+ */
51
+ static parseRecordFields(bytes) {
52
+ const text = PrintableTextDecoder.decodeBytes(bytes)
53
+ .replace(/\u0000/gu, '')
54
+ .replace(/\r\n?/gu, '\n')
55
+ .trim()
56
+ const fields = {}
57
+
58
+ for (const line of text.split('\n')) {
59
+ for (const segment of line.split('|')) {
60
+ const candidate = segment.trim()
61
+ const separatorIndex = candidate.indexOf('=')
62
+
63
+ if (separatorIndex <= 0) {
64
+ continue
65
+ }
66
+
67
+ const key = candidate
68
+ .slice(0, separatorIndex)
69
+ .trim()
70
+ .toUpperCase()
71
+ if (!key) {
72
+ continue
73
+ }
74
+
75
+ fields[key] = candidate.slice(separatorIndex + 1).trim()
76
+ }
77
+ }
78
+
79
+ return fields
80
+ }
81
+
82
+ /**
83
+ * Returns one field value using the first matching key.
84
+ * @param {Record<string, string>} fields
85
+ * @param {string[]} keys
86
+ * @returns {string}
87
+ */
88
+ static firstField(fields, keys) {
89
+ for (const key of keys) {
90
+ const value = fields[String(key).toUpperCase()]
91
+ if (value !== undefined && value !== '') {
92
+ return value
93
+ }
94
+ }
95
+
96
+ return ''
97
+ }
98
+
99
+ /**
100
+ * Parses one integer-like field value.
101
+ * @param {string | undefined} value
102
+ * @returns {number | null}
103
+ */
104
+ static parseInteger(value) {
105
+ const parsed = Number(String(value ?? '').trim())
106
+ return Number.isInteger(parsed) ? parsed : null
107
+ }
108
+
109
+ /**
110
+ * Parses one numeric field value, including simple unit suffixes.
111
+ * @param {string | undefined} value
112
+ * @returns {number | null}
113
+ */
114
+ static parseNumber(value) {
115
+ const match = String(value ?? '')
116
+ .trim()
117
+ .match(/-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/i)
118
+
119
+ if (!match) {
120
+ return null
121
+ }
122
+
123
+ const parsed = Number(match[0])
124
+ return Number.isFinite(parsed) ? parsed : null
125
+ }
126
+
127
+ /**
128
+ * Parses an Altium boolean token.
129
+ * @param {string | undefined} value
130
+ * @returns {boolean | null}
131
+ */
132
+ static parseBoolean(value) {
133
+ const normalized = String(value ?? '')
134
+ .trim()
135
+ .toUpperCase()
136
+
137
+ if (['T', 'TRUE', '1', 'YES'].includes(normalized)) {
138
+ return true
139
+ }
140
+ if (['F', 'FALSE', '0', 'NO'].includes(normalized)) {
141
+ return false
142
+ }
143
+
144
+ return null
145
+ }
146
+
147
+ /**
148
+ * Reads one little-endian unsigned integer from a byte view.
149
+ * @param {Uint8Array} bytes
150
+ * @param {number} offset
151
+ * @returns {number}
152
+ */
153
+ static readUint32(bytes, offset) {
154
+ return new DataView(
155
+ bytes.buffer,
156
+ bytes.byteOffset + offset,
157
+ 4
158
+ ).getUint32(0, true)
159
+ }
160
+
161
+ /**
162
+ * Normalizes one byte-like input into a Uint8Array view.
163
+ * @param {Uint8Array | ArrayBuffer | undefined} bytes
164
+ * @returns {Uint8Array}
165
+ */
166
+ static toUint8Array(bytes) {
167
+ if (!bytes) {
168
+ return new Uint8Array(0)
169
+ }
170
+
171
+ if (bytes instanceof Uint8Array) {
172
+ return bytes
173
+ }
174
+
175
+ return new Uint8Array(bytes)
176
+ }
177
+ }