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,173 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
6
+ import { ParserUtils } from './ParserUtils.mjs'
7
+
8
+ const { getField, parseBoolean, parseNumericField } = ParserUtils
9
+
10
+ /**
11
+ * Normalizes embedded and external schematic image records.
12
+ */
13
+ export class SchematicImageParser {
14
+ /**
15
+ * Parses schematic image records and resolves embedded payloads when the
16
+ * file is an OLE container.
17
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
18
+ * @param {ArrayBuffer} arrayBuffer
19
+ * @returns {{ images: { x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string }[], diagnostics: { severity: 'info' | 'warning', message: string }[] }}
20
+ */
21
+ static parseSchematicImages(records, arrayBuffer) {
22
+ const diagnostics = []
23
+ const imageRecords = records.filter(
24
+ (record) => getField(record.fields, 'RECORD') === '30'
25
+ )
26
+ let oleDocument = null
27
+
28
+ if (
29
+ imageRecords.some((record) =>
30
+ SchematicImageParser.#isEmbedded(record.fields)
31
+ )
32
+ ) {
33
+ try {
34
+ oleDocument = OleCompoundDocument.fromArrayBuffer(arrayBuffer)
35
+ } catch {
36
+ oleDocument = null
37
+ }
38
+ }
39
+
40
+ const images = imageRecords
41
+ .map((record) =>
42
+ SchematicImageParser.#parseSchematicImageRecord(
43
+ record,
44
+ oleDocument,
45
+ diagnostics
46
+ )
47
+ )
48
+ .filter(Boolean)
49
+
50
+ return { images, diagnostics }
51
+ }
52
+
53
+ /**
54
+ * Returns true when one record requests an embedded image payload.
55
+ * @param {Record<string, string | string[]>} fields
56
+ * @returns {boolean}
57
+ */
58
+ static #isEmbedded(fields) {
59
+ return parseBoolean(fields.EmbedImage || fields.EMBEDIMAGE)
60
+ }
61
+
62
+ /**
63
+ * Normalizes one image placement record.
64
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
65
+ * @param {OleCompoundDocument | null} oleDocument
66
+ * @param {{ severity: 'info' | 'warning', message: string }[]} diagnostics
67
+ * @returns {{ x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string } | null}
68
+ */
69
+ static #parseSchematicImageRecord(record, oleDocument, diagnostics) {
70
+ const x = parseNumericField(record.fields, 'Location.X')
71
+ const y = parseNumericField(record.fields, 'Location.Y')
72
+ const cornerX = parseNumericField(record.fields, 'Corner.X')
73
+ const cornerY = parseNumericField(record.fields, 'Corner.Y')
74
+
75
+ if (x === null || y === null || cornerX === null || cornerY === null) {
76
+ return null
77
+ }
78
+
79
+ const fileName =
80
+ getField(record.fields, 'FileName') ||
81
+ getField(record.fields, 'FILENAME')
82
+ const embedded = SchematicImageParser.#isEmbedded(record.fields)
83
+ const keepAspect = parseBoolean(
84
+ record.fields.KeepAspect || record.fields.KEEPASPECT
85
+ )
86
+ const renderOrder =
87
+ parseNumericField(record.fields, 'IndexInSheet') ??
88
+ record.recordIndex
89
+ let mimeType = ''
90
+ let dataBase64 = ''
91
+ let diagnosticState = embedded ? 'missing-embedded-payload' : 'external'
92
+
93
+ if (embedded && fileName && oleDocument) {
94
+ try {
95
+ const streamBytes = oleDocument.getStream(fileName)
96
+ mimeType = SchematicImageParser.#inferMimeType(fileName)
97
+ dataBase64 = SchematicImageParser.#encodeBase64(streamBytes)
98
+ diagnosticState = 'embedded'
99
+ } catch {
100
+ diagnostics.push({
101
+ severity: 'warning',
102
+ message:
103
+ 'Embedded schematic image payload could not be resolved for ' +
104
+ fileName +
105
+ '.'
106
+ })
107
+ }
108
+ } else if (embedded) {
109
+ diagnostics.push({
110
+ severity: 'warning',
111
+ message:
112
+ 'Embedded schematic image payload could not be resolved for ' +
113
+ (fileName || 'unnamed image') +
114
+ '.'
115
+ })
116
+ }
117
+
118
+ return {
119
+ x,
120
+ y,
121
+ cornerX,
122
+ cornerY,
123
+ fileName,
124
+ embedded,
125
+ keepAspect,
126
+ mimeType,
127
+ dataBase64,
128
+ renderOrder,
129
+ diagnosticState
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Infers a MIME type from one file name.
135
+ * @param {string} fileName
136
+ * @returns {string}
137
+ */
138
+ static #inferMimeType(fileName) {
139
+ const normalized = String(fileName || '').toLowerCase()
140
+
141
+ if (normalized.endsWith('.bmp')) return 'image/bmp'
142
+ if (normalized.endsWith('.gif')) return 'image/gif'
143
+ if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg')) {
144
+ return 'image/jpeg'
145
+ }
146
+ if (normalized.endsWith('.png')) return 'image/png'
147
+ if (normalized.endsWith('.svg')) return 'image/svg+xml'
148
+ if (normalized.endsWith('.tif') || normalized.endsWith('.tiff')) {
149
+ return 'image/tiff'
150
+ }
151
+
152
+ return ''
153
+ }
154
+
155
+ /**
156
+ * Encodes one byte array as base64 in both browser and test environments.
157
+ * @param {Uint8Array} bytes
158
+ * @returns {string}
159
+ */
160
+ static #encodeBase64(bytes) {
161
+ if (typeof Buffer !== 'undefined') {
162
+ return Buffer.from(bytes).toString('base64')
163
+ }
164
+
165
+ let binary = ''
166
+
167
+ for (const byte of bytes) {
168
+ binary += String.fromCharCode(byte)
169
+ }
170
+
171
+ return btoa(binary)
172
+ }
173
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ const { getField, parseNumericField, toColor } = ParserUtils
8
+
9
+ /**
10
+ * Normalizes explicit schematic junction records.
11
+ */
12
+ export class SchematicJunctionParser {
13
+ /**
14
+ * Parses authored schematic junction dots from `RECORD=29`.
15
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
16
+ * @returns {{ x: number, y: number, color: string, renderOrder: number }[]}
17
+ */
18
+ static parseSchematicJunctions(records) {
19
+ return records
20
+ .map((record) => {
21
+ if (getField(record.fields, 'RECORD') !== '29') {
22
+ return null
23
+ }
24
+
25
+ const x = parseNumericField(record.fields, 'Location.X')
26
+ const y = parseNumericField(record.fields, 'Location.Y')
27
+
28
+ if (x === null || y === null) {
29
+ return null
30
+ }
31
+
32
+ return {
33
+ x,
34
+ y,
35
+ color: toColor(record.fields.Color, '#000080'),
36
+ renderOrder:
37
+ parseNumericField(record.fields, 'IndexInSheet') ??
38
+ record.recordIndex
39
+ }
40
+ })
41
+ .filter(Boolean)
42
+ }
43
+ }