altium-toolkit 0.1.0

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 (82) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +20 -0
  3. package/CONTRIBUTING.md +19 -0
  4. package/LICENSE +22 -0
  5. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  6. package/LICENSES/GPL-3.0-or-later.txt +232 -0
  7. package/NOTICE.md +32 -0
  8. package/README.md +116 -0
  9. package/docs/api.md +73 -0
  10. package/docs/model-format.md +36 -0
  11. package/docs/testing.md +25 -0
  12. package/examples/README.md +47 -0
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
  14. package/examples/arduino-uno/SvgViewportController.mjs +306 -0
  15. package/examples/arduino-uno/example.mjs +480 -0
  16. package/examples/arduino-uno/index.html +163 -0
  17. package/examples/arduino-uno/styles.css +552 -0
  18. package/examples/server.mjs +212 -0
  19. package/package.json +53 -0
  20. package/spec/library-scope.md +32 -0
  21. package/src/core/BinaryReader.mjs +127 -0
  22. package/src/core/altium/AltiumLayoutParser.mjs +485 -0
  23. package/src/core/altium/AltiumParser.mjs +1007 -0
  24. package/src/core/altium/AsciiRecordParser.mjs +151 -0
  25. package/src/core/altium/ParserUtils.mjs +173 -0
  26. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
  27. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
  28. package/src/core/altium/PcbModelParser.mjs +336 -0
  29. package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
  30. package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
  31. package/src/core/altium/PcbStreamExtractor.mjs +210 -0
  32. package/src/core/altium/PrintableTextDecoder.mjs +156 -0
  33. package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
  34. package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
  35. package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
  36. package/src/core/altium/SchematicImageParser.mjs +173 -0
  37. package/src/core/altium/SchematicJunctionParser.mjs +43 -0
  38. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
  39. package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
  40. package/src/core/altium/SchematicPinParser.mjs +767 -0
  41. package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
  42. package/src/core/altium/SchematicSheetParser.mjs +241 -0
  43. package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
  44. package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
  45. package/src/core/altium/SchematicTextParser.mjs +708 -0
  46. package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
  47. package/src/core/ole/OleCompoundDocument.mjs +439 -0
  48. package/src/core/ole/OleConstants.mjs +64 -0
  49. package/src/core/ole/OleDirectoryEntry.mjs +95 -0
  50. package/src/index.mjs +7 -0
  51. package/src/parser.mjs +21 -0
  52. package/src/renderers.mjs +15 -0
  53. package/src/scene3d.mjs +9 -0
  54. package/src/styles/altium-renderers.css +358 -0
  55. package/src/ui/BomTableRenderer.mjs +46 -0
  56. package/src/ui/PcbArcUtils.mjs +189 -0
  57. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
  58. package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
  59. package/src/ui/PcbScene3dBuilder.mjs +742 -0
  60. package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
  61. package/src/ui/PcbScene3dPackages.mjs +137 -0
  62. package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
  63. package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
  64. package/src/ui/PcbSvgRenderer.mjs +906 -0
  65. package/src/ui/SchematicColorResolver.mjs +132 -0
  66. package/src/ui/SchematicContentLayout.mjs +661 -0
  67. package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
  68. package/src/ui/SchematicImageRenderer.mjs +135 -0
  69. package/src/ui/SchematicJunctionRenderer.mjs +381 -0
  70. package/src/ui/SchematicNoteRenderer.mjs +427 -0
  71. package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
  72. package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
  73. package/src/ui/SchematicPortRenderer.mjs +558 -0
  74. package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
  75. package/src/ui/SchematicRegionRenderer.mjs +94 -0
  76. package/src/ui/SchematicShapeRenderer.mjs +398 -0
  77. package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
  78. package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +756 -0
  80. package/src/ui/SchematicSvgUtils.mjs +182 -0
  81. package/src/ui/SchematicTypography.mjs +204 -0
  82. package/src/workers/altium-parser.worker.mjs +29 -0
@@ -0,0 +1,424 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Decodes fixed-size binary PCB primitive streams recovered from OLE-backed
7
+ * PcbDoc files.
8
+ */
9
+ export class PcbBinaryPrimitiveParser {
10
+ static #ARC_OBJECT_ID = 1
11
+
12
+ static #ARC_RECORD_MIN_BYTE_LENGTH = 45
13
+
14
+ static #TRACK_OBJECT_ID = 4
15
+
16
+ static #PAD_OBJECT_ID = 2
17
+
18
+ static #PAD_SUBRECORD_COUNT = 6
19
+
20
+ static #PAD_MAIN_SUBRECORD_INDEX = 4
21
+
22
+ static #PAD_EXTENSION_SUBRECORD_INDEX = 5
23
+
24
+ static #PAD_MAIN_RECORD_MIN_BYTE_LENGTH = 61
25
+
26
+ static #PAD_EXTENSION_MIN_BYTE_LENGTH = 596
27
+
28
+ /**
29
+ * Decodes one length-prefixed track stream.
30
+ * @param {Uint8Array | ArrayBuffer} headerBytes
31
+ * @param {Uint8Array | ArrayBuffer} dataBytes
32
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, width: number, layerCode: number, layerId: number }[]}
33
+ */
34
+ static parseTrackStream(headerBytes, dataBytes) {
35
+ const count = PcbBinaryPrimitiveParser.#readRecordCount(headerBytes)
36
+ const normalizedData = PcbBinaryPrimitiveParser.#toUint8Array(dataBytes)
37
+
38
+ if (!count) {
39
+ return []
40
+ }
41
+
42
+ let offset = 0
43
+ const tracks = []
44
+
45
+ for (let index = 0; index < count; index += 1) {
46
+ if (offset + 5 > normalizedData.byteLength) {
47
+ return []
48
+ }
49
+
50
+ const objectId = normalizedData[offset]
51
+ offset += 1
52
+
53
+ if (objectId !== PcbBinaryPrimitiveParser.#TRACK_OBJECT_ID) {
54
+ return []
55
+ }
56
+
57
+ const payloadLength = new DataView(
58
+ normalizedData.buffer,
59
+ normalizedData.byteOffset + offset,
60
+ 4
61
+ ).getUint32(0, true)
62
+ offset += 4
63
+
64
+ if (offset + payloadLength > normalizedData.byteLength) {
65
+ return []
66
+ }
67
+
68
+ const payload = new DataView(
69
+ normalizedData.buffer,
70
+ normalizedData.byteOffset + offset,
71
+ payloadLength
72
+ )
73
+ const layerId = payload.getUint8(0)
74
+
75
+ tracks.push({
76
+ x1: PcbBinaryPrimitiveParser.#readMil(payload, 13),
77
+ y1: PcbBinaryPrimitiveParser.#readMil(payload, 17),
78
+ x2: PcbBinaryPrimitiveParser.#readMil(payload, 21),
79
+ y2: PcbBinaryPrimitiveParser.#readMil(payload, 25),
80
+ width: PcbBinaryPrimitiveParser.#readMil(payload, 29),
81
+ layerCode: layerId,
82
+ layerId
83
+ })
84
+
85
+ offset += payloadLength
86
+ }
87
+
88
+ return tracks
89
+ }
90
+
91
+ /**
92
+ * Decodes one fixed-size via stream.
93
+ * @param {Uint8Array | ArrayBuffer} headerBytes
94
+ * @param {Uint8Array | ArrayBuffer} dataBytes
95
+ * @returns {{ x: number, y: number, diameter: number, holeDiameter: number }[]}
96
+ */
97
+ static parseViaStream(headerBytes, dataBytes) {
98
+ return PcbBinaryPrimitiveParser.#sliceFixedRecords(
99
+ headerBytes,
100
+ dataBytes,
101
+ 326
102
+ ).map((view) => ({
103
+ x: PcbBinaryPrimitiveParser.#readMil(view, 18),
104
+ y: PcbBinaryPrimitiveParser.#readMil(view, 22),
105
+ diameter: PcbBinaryPrimitiveParser.#readMil(view, 26),
106
+ holeDiameter: PcbBinaryPrimitiveParser.#readMil(view, 30)
107
+ }))
108
+ }
109
+
110
+ /**
111
+ * Decodes one fixed-size fill stream.
112
+ * @param {Uint8Array | ArrayBuffer} headerBytes
113
+ * @param {Uint8Array | ArrayBuffer} dataBytes
114
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, layerCode: number, layerId: number }[]}
115
+ */
116
+ static parseFillStream(headerBytes, dataBytes) {
117
+ return PcbBinaryPrimitiveParser.#sliceFixedRecords(
118
+ headerBytes,
119
+ dataBytes,
120
+ 55
121
+ ).map((view) => ({
122
+ x1: PcbBinaryPrimitiveParser.#readMil(view, 18),
123
+ y1: PcbBinaryPrimitiveParser.#readMil(view, 22),
124
+ x2: PcbBinaryPrimitiveParser.#readMil(view, 26),
125
+ y2: PcbBinaryPrimitiveParser.#readMil(view, 30),
126
+ layerCode: view.getUint16(46, true),
127
+ layerId: view.getUint8(5)
128
+ }))
129
+ }
130
+
131
+ /**
132
+ * Decodes one length-prefixed arc stream.
133
+ * @param {Uint8Array | ArrayBuffer} headerBytes
134
+ * @param {Uint8Array | ArrayBuffer} dataBytes
135
+ * @returns {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode: number, layerId: number }[]}
136
+ */
137
+ static parseArcStream(headerBytes, dataBytes) {
138
+ const count = PcbBinaryPrimitiveParser.#readRecordCount(headerBytes)
139
+ const normalizedData = PcbBinaryPrimitiveParser.#toUint8Array(dataBytes)
140
+
141
+ if (!count) {
142
+ return []
143
+ }
144
+
145
+ let offset = 0
146
+ const arcs = []
147
+
148
+ for (let index = 0; index < count; index += 1) {
149
+ if (offset + 5 > normalizedData.byteLength) {
150
+ return []
151
+ }
152
+
153
+ const objectId = normalizedData[offset]
154
+ offset += 1
155
+
156
+ if (objectId !== PcbBinaryPrimitiveParser.#ARC_OBJECT_ID) {
157
+ return []
158
+ }
159
+
160
+ const payloadLength = new DataView(
161
+ normalizedData.buffer,
162
+ normalizedData.byteOffset + offset,
163
+ 4
164
+ ).getUint32(0, true)
165
+ offset += 4
166
+
167
+ if (
168
+ payloadLength <
169
+ PcbBinaryPrimitiveParser.#ARC_RECORD_MIN_BYTE_LENGTH ||
170
+ offset + payloadLength > normalizedData.byteLength
171
+ ) {
172
+ return []
173
+ }
174
+
175
+ const payload = new DataView(
176
+ normalizedData.buffer,
177
+ normalizedData.byteOffset + offset,
178
+ payloadLength
179
+ )
180
+ const layerId = payload.getUint8(0)
181
+
182
+ arcs.push({
183
+ x: PcbBinaryPrimitiveParser.#readMil(payload, 13),
184
+ y: PcbBinaryPrimitiveParser.#readMil(payload, 17),
185
+ radius: PcbBinaryPrimitiveParser.#readMil(payload, 21),
186
+ startAngle: payload.getFloat64(25, true),
187
+ endAngle: payload.getFloat64(33, true),
188
+ width: PcbBinaryPrimitiveParser.#readMil(payload, 41),
189
+ layerCode: layerId,
190
+ layerId
191
+ })
192
+
193
+ offset += payloadLength
194
+ }
195
+
196
+ return arcs
197
+ }
198
+
199
+ /**
200
+ * Decodes one variable-length pad stream.
201
+ * @param {Uint8Array | ArrayBuffer} headerBytes
202
+ * @param {Uint8Array | ArrayBuffer} dataBytes
203
+ * @returns {{ x: number, y: number, sizeTopX: number, sizeTopY: number, sizeMidX: number, sizeMidY: number, sizeBottomX: number, sizeBottomY: number, holeDiameter: number, shapeTop: number, shapeMid: number, shapeBottom: number, rotation: number, isPlated: boolean, holeShape: number | null, holeSlotLength: number | null, holeRotation: number | null, hasRoundedRect: boolean, roundedRectShapeTop: number | null, cornerRadiusTop: number | null, offsetTopX: number, offsetTopY: number }[]}
204
+ */
205
+ static parsePadStream(headerBytes, dataBytes) {
206
+ const count = PcbBinaryPrimitiveParser.#readRecordCount(headerBytes)
207
+ const normalizedData = PcbBinaryPrimitiveParser.#toUint8Array(dataBytes)
208
+
209
+ if (!count) {
210
+ return []
211
+ }
212
+
213
+ let offset = 0
214
+ const pads = []
215
+
216
+ for (let index = 0; index < count; index += 1) {
217
+ if (offset + 1 > normalizedData.byteLength) {
218
+ return []
219
+ }
220
+
221
+ const objectId = normalizedData[offset]
222
+ offset += 1
223
+
224
+ if (objectId !== PcbBinaryPrimitiveParser.#PAD_OBJECT_ID) {
225
+ return []
226
+ }
227
+
228
+ const subrecords = []
229
+
230
+ for (
231
+ let subrecordIndex = 0;
232
+ subrecordIndex < PcbBinaryPrimitiveParser.#PAD_SUBRECORD_COUNT;
233
+ subrecordIndex += 1
234
+ ) {
235
+ if (offset + 4 > normalizedData.byteLength) {
236
+ return []
237
+ }
238
+
239
+ const subrecordLength = new DataView(
240
+ normalizedData.buffer,
241
+ normalizedData.byteOffset + offset,
242
+ 4
243
+ ).getUint32(0, true)
244
+ offset += 4
245
+
246
+ if (offset + subrecordLength > normalizedData.byteLength) {
247
+ return []
248
+ }
249
+
250
+ subrecords.push(
251
+ new DataView(
252
+ normalizedData.buffer,
253
+ normalizedData.byteOffset + offset,
254
+ subrecordLength
255
+ )
256
+ )
257
+ offset += subrecordLength
258
+ }
259
+
260
+ const pad = PcbBinaryPrimitiveParser.#parsePadSubrecords(subrecords)
261
+
262
+ if (!pad) {
263
+ return []
264
+ }
265
+
266
+ pads.push(pad)
267
+ }
268
+
269
+ return pads
270
+ }
271
+
272
+ /**
273
+ * Splits one fixed-length record stream into DataView slices.
274
+ * @param {Uint8Array | ArrayBuffer} headerBytes
275
+ * @param {Uint8Array | ArrayBuffer} dataBytes
276
+ * @param {number} recordByteLength
277
+ * @returns {DataView[]}
278
+ */
279
+ static #sliceFixedRecords(headerBytes, dataBytes, recordByteLength) {
280
+ const normalizedData = PcbBinaryPrimitiveParser.#toUint8Array(dataBytes)
281
+ const count = PcbBinaryPrimitiveParser.#readRecordCount(headerBytes)
282
+
283
+ if (!count) {
284
+ return []
285
+ }
286
+
287
+ if (normalizedData.byteLength < count * recordByteLength) {
288
+ return []
289
+ }
290
+
291
+ const views = []
292
+
293
+ for (let index = 0; index < count; index += 1) {
294
+ views.push(
295
+ new DataView(
296
+ normalizedData.buffer,
297
+ normalizedData.byteOffset + index * recordByteLength,
298
+ recordByteLength
299
+ )
300
+ )
301
+ }
302
+
303
+ return views
304
+ }
305
+
306
+ /**
307
+ * Decodes one pad payload from its subrecords.
308
+ * @param {DataView[]} subrecords
309
+ * @returns {{ x: number, y: number, sizeTopX: number, sizeTopY: number, sizeMidX: number, sizeMidY: number, sizeBottomX: number, sizeBottomY: number, holeDiameter: number, shapeTop: number, shapeMid: number, shapeBottom: number, rotation: number, isPlated: boolean, holeShape: number | null, holeSlotLength: number | null, holeRotation: number | null, hasRoundedRect: boolean, roundedRectShapeTop: number | null, cornerRadiusTop: number | null, offsetTopX: number, offsetTopY: number } | null}
310
+ */
311
+ static #parsePadSubrecords(subrecords) {
312
+ const mainRecord =
313
+ subrecords[PcbBinaryPrimitiveParser.#PAD_MAIN_SUBRECORD_INDEX]
314
+ const extensionRecord =
315
+ subrecords[PcbBinaryPrimitiveParser.#PAD_EXTENSION_SUBRECORD_INDEX]
316
+
317
+ if (
318
+ !mainRecord ||
319
+ mainRecord.byteLength <
320
+ PcbBinaryPrimitiveParser.#PAD_MAIN_RECORD_MIN_BYTE_LENGTH
321
+ ) {
322
+ return null
323
+ }
324
+
325
+ return {
326
+ x: PcbBinaryPrimitiveParser.#readMil(mainRecord, 13),
327
+ y: PcbBinaryPrimitiveParser.#readMil(mainRecord, 17),
328
+ sizeTopX: PcbBinaryPrimitiveParser.#readMil(mainRecord, 21),
329
+ sizeTopY: PcbBinaryPrimitiveParser.#readMil(mainRecord, 25),
330
+ sizeMidX: PcbBinaryPrimitiveParser.#readMil(mainRecord, 29),
331
+ sizeMidY: PcbBinaryPrimitiveParser.#readMil(mainRecord, 33),
332
+ sizeBottomX: PcbBinaryPrimitiveParser.#readMil(mainRecord, 37),
333
+ sizeBottomY: PcbBinaryPrimitiveParser.#readMil(mainRecord, 41),
334
+ holeDiameter: PcbBinaryPrimitiveParser.#readMil(mainRecord, 45),
335
+ shapeTop: mainRecord.getUint8(49),
336
+ shapeMid: mainRecord.getUint8(50),
337
+ shapeBottom: mainRecord.getUint8(51),
338
+ rotation: mainRecord.getFloat64(52, true),
339
+ isPlated: mainRecord.getUint8(60) !== 0,
340
+ ...PcbBinaryPrimitiveParser.#parsePadExtensionBlock(extensionRecord)
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Decodes one optional pad extension block.
346
+ * @param {DataView | undefined} extensionRecord
347
+ * @returns {{ holeShape: number | null, holeSlotLength: number | null, holeRotation: number | null, hasRoundedRect: boolean, roundedRectShapeTop: number | null, cornerRadiusTop: number | null, offsetTopX: number, offsetTopY: number }}
348
+ */
349
+ static #parsePadExtensionBlock(extensionRecord) {
350
+ if (
351
+ !extensionRecord ||
352
+ extensionRecord.byteLength <
353
+ PcbBinaryPrimitiveParser.#PAD_EXTENSION_MIN_BYTE_LENGTH
354
+ ) {
355
+ return {
356
+ holeShape: null,
357
+ holeSlotLength: null,
358
+ holeRotation: null,
359
+ hasRoundedRect: false,
360
+ roundedRectShapeTop: null,
361
+ cornerRadiusTop: null,
362
+ offsetTopX: 0,
363
+ offsetTopY: 0
364
+ }
365
+ }
366
+
367
+ return {
368
+ holeShape: extensionRecord.getUint8(262),
369
+ holeSlotLength: PcbBinaryPrimitiveParser.#readMil(
370
+ extensionRecord,
371
+ 263
372
+ ),
373
+ holeRotation: extensionRecord.getFloat64(267, true),
374
+ hasRoundedRect: extensionRecord.getUint8(531) !== 0,
375
+ roundedRectShapeTop: extensionRecord.getUint8(532),
376
+ cornerRadiusTop: extensionRecord.getUint8(564),
377
+ offsetTopX: PcbBinaryPrimitiveParser.#readMil(extensionRecord, 275),
378
+ offsetTopY: PcbBinaryPrimitiveParser.#readMil(extensionRecord, 403)
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Normalizes one byte-like input into a Uint8Array view.
384
+ * @param {Uint8Array | ArrayBuffer} bytes
385
+ * @returns {Uint8Array}
386
+ */
387
+ static #toUint8Array(bytes) {
388
+ if (bytes instanceof Uint8Array) {
389
+ return bytes
390
+ }
391
+
392
+ return new Uint8Array(bytes)
393
+ }
394
+
395
+ /**
396
+ * Reads one little-endian record count from a binary stream header.
397
+ * @param {Uint8Array | ArrayBuffer} headerBytes
398
+ * @returns {number}
399
+ */
400
+ static #readRecordCount(headerBytes) {
401
+ const normalizedHeader =
402
+ PcbBinaryPrimitiveParser.#toUint8Array(headerBytes)
403
+
404
+ if (normalizedHeader.byteLength < 4) {
405
+ return 0
406
+ }
407
+
408
+ return new DataView(
409
+ normalizedHeader.buffer,
410
+ normalizedHeader.byteOffset,
411
+ normalizedHeader.byteLength
412
+ ).getUint32(0, true)
413
+ }
414
+
415
+ /**
416
+ * Reads one signed fixed-point mil coordinate.
417
+ * @param {DataView} view
418
+ * @param {number} offset
419
+ * @returns {number}
420
+ */
421
+ static #readMil(view, offset) {
422
+ return view.getInt32(offset, true) / 10000
423
+ }
424
+ }