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,241 @@
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, parseBoolean, parseNumericField, toColor } = ParserUtils
8
+
9
+ /**
10
+ * Normalizes schematic sheet symbols and sheet entries.
11
+ */
12
+ export class SchematicSheetParser {
13
+ /**
14
+ * Parses sheet symbols and their child entries from schematic records.
15
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
16
+ * @returns {{ sheetSymbols: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null }[], sheetEntries: { ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number }[] }}
17
+ */
18
+ static parse(records) {
19
+ const sheetSymbols = records
20
+ .map((record) =>
21
+ SchematicSheetParser.#parseSheetSymbolRecord(record)
22
+ )
23
+ .filter(Boolean)
24
+ const symbolLookup =
25
+ SchematicSheetParser.#buildSheetSymbolLookup(sheetSymbols)
26
+ const sheetEntries = records
27
+ .map((record) =>
28
+ SchematicSheetParser.#parseSheetEntryRecord(
29
+ record,
30
+ symbolLookup
31
+ )
32
+ )
33
+ .filter(Boolean)
34
+
35
+ return { sheetSymbols, sheetEntries }
36
+ }
37
+
38
+ /**
39
+ * Normalizes one `RECORD=15` sheet symbol.
40
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
41
+ * @returns {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null } | null}
42
+ */
43
+ static #parseSheetSymbolRecord(record) {
44
+ if (getField(record.fields, 'RECORD') !== '15') {
45
+ return null
46
+ }
47
+
48
+ const x = parseNumericField(record.fields, 'Location.X')
49
+ const y = parseNumericField(record.fields, 'Location.Y')
50
+ const width = parseNumericField(record.fields, 'XSize')
51
+ const height = parseNumericField(record.fields, 'YSize')
52
+
53
+ if (x === null || y === null || width === null || height === null) {
54
+ return null
55
+ }
56
+
57
+ const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
58
+
59
+ return {
60
+ x,
61
+ y,
62
+ width,
63
+ height,
64
+ color: toColor(record.fields.Color, '#a44a1b'),
65
+ fill: toColor(record.fields.AreaColor, '#ffe16f'),
66
+ isSolid: parseBoolean(record.fields.IsSolid),
67
+ transparent: parseBoolean(record.fields.Transparent),
68
+ ownerIndex: getField(record.fields, 'OwnerIndex') || undefined,
69
+ uniqueId: getField(record.fields, 'UniqueId'),
70
+ renderOrder: indexInSheet ?? record.recordIndex,
71
+ sourceRecordIndex: record.recordIndex,
72
+ indexInSheet
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Builds a lookup that tolerates the owner-index variants found in
78
+ * recovered Altium records.
79
+ * @param {{ sourceRecordIndex: number, indexInSheet: number | null }[]} sheetSymbols
80
+ * @returns {Map<string, any>}
81
+ */
82
+ static #buildSheetSymbolLookup(sheetSymbols) {
83
+ const lookup = new Map()
84
+
85
+ for (const sheetSymbol of sheetSymbols) {
86
+ const candidateKeys = new Set([
87
+ String(sheetSymbol.sourceRecordIndex),
88
+ String(sheetSymbol.sourceRecordIndex + 1)
89
+ ])
90
+
91
+ if (sheetSymbol.indexInSheet !== null) {
92
+ candidateKeys.add(String(sheetSymbol.indexInSheet))
93
+ candidateKeys.add(String(sheetSymbol.indexInSheet + 1))
94
+ }
95
+
96
+ for (const key of candidateKeys) {
97
+ lookup.set(key, sheetSymbol)
98
+ }
99
+ }
100
+
101
+ return lookup
102
+ }
103
+
104
+ /**
105
+ * Normalizes one `RECORD=16` sheet entry against its parent symbol.
106
+ * @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
107
+ * @param {Map<string, { x: number, y: number, width: number, height: number }>} symbolLookup
108
+ * @returns {{ ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number } | null}
109
+ */
110
+ static #parseSheetEntryRecord(record, symbolLookup) {
111
+ if (getField(record.fields, 'RECORD') !== '16') {
112
+ return null
113
+ }
114
+
115
+ const ownerIndex = getField(record.fields, 'OwnerIndex')
116
+ const name = getField(record.fields, 'Name')
117
+ const parentSymbol = symbolLookup.get(ownerIndex)
118
+
119
+ if (!ownerIndex || !name || !parentSymbol) {
120
+ return null
121
+ }
122
+
123
+ const side = SchematicSheetParser.#resolveSheetEntrySide(
124
+ parseNumericField(record.fields, 'Side')
125
+ )
126
+ const distance = SchematicSheetParser.#parseSheetEntryDistance(
127
+ record.fields
128
+ )
129
+ const point = SchematicSheetParser.#resolveSheetEntryPoint(
130
+ parentSymbol,
131
+ side,
132
+ distance
133
+ )
134
+
135
+ return {
136
+ ownerIndex,
137
+ name,
138
+ side,
139
+ direction: SchematicSheetParser.#resolveSheetEntryDirection(
140
+ parseNumericField(record.fields, 'IOType')
141
+ ),
142
+ style: parseNumericField(record.fields, 'Style') || 0,
143
+ x: point.x,
144
+ y: point.y,
145
+ color: toColor(record.fields.Color, '#a44a1b'),
146
+ fill: toColor(record.fields.AreaColor, '#ffe16f'),
147
+ textColor: toColor(
148
+ record.fields.TextColor || record.fields.Color,
149
+ '#2c3134'
150
+ ),
151
+ harnessType: getField(record.fields, 'HarnessType'),
152
+ renderOrder:
153
+ parseNumericField(record.fields, 'IndexInSheet') ??
154
+ record.recordIndex
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Parses Altium's `DistanceFromTop` plus optional fractional companion.
160
+ * @param {Record<string, string | string[]>} fields
161
+ * @returns {number}
162
+ */
163
+ static #parseSheetEntryDistance(fields) {
164
+ const whole = parseNumericField(fields, 'DistanceFromTop') || 0
165
+ const fraction = parseNumericField(fields, 'DistanceFromTop_FRAC1') || 0
166
+ const sign = whole < 0 ? -1 : 1
167
+
168
+ return whole * 10 + (fraction / 100000) * sign
169
+ }
170
+
171
+ /**
172
+ * Resolves one entry point on the parent sheet symbol perimeter.
173
+ * @param {{ x: number, y: number, width: number, height: number }} parentSymbol
174
+ * @param {'left' | 'right' | 'top' | 'bottom'} side
175
+ * @param {number} distance
176
+ * @returns {{ x: number, y: number }}
177
+ */
178
+ static #resolveSheetEntryPoint(parentSymbol, side, distance) {
179
+ switch (side) {
180
+ case 'right':
181
+ return {
182
+ x: parentSymbol.x + parentSymbol.width,
183
+ y: parentSymbol.y - distance
184
+ }
185
+ case 'top':
186
+ return {
187
+ x: parentSymbol.x + distance,
188
+ y: parentSymbol.y
189
+ }
190
+ case 'bottom':
191
+ return {
192
+ x: parentSymbol.x + distance,
193
+ y: parentSymbol.y - parentSymbol.height
194
+ }
195
+ case 'left':
196
+ default:
197
+ return {
198
+ x: parentSymbol.x,
199
+ y: parentSymbol.y - distance
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Resolves a sheet-entry side code into a readable label.
206
+ * @param {number | null} side
207
+ * @returns {'left' | 'right' | 'top' | 'bottom'}
208
+ */
209
+ static #resolveSheetEntrySide(side) {
210
+ switch (side) {
211
+ case 1:
212
+ return 'right'
213
+ case 2:
214
+ return 'top'
215
+ case 3:
216
+ return 'bottom'
217
+ case 0:
218
+ default:
219
+ return 'left'
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Resolves an Altium I/O code into a readable direction label.
225
+ * @param {number | null} ioType
226
+ * @returns {'unspecified' | 'output' | 'input' | 'bidirectional'}
227
+ */
228
+ static #resolveSheetEntryDirection(ioType) {
229
+ switch (ioType) {
230
+ case 1:
231
+ return 'output'
232
+ case 2:
233
+ return 'input'
234
+ case 3:
235
+ return 'bidirectional'
236
+ case 0:
237
+ default:
238
+ return 'unspecified'
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,46 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Resolves standard schematic sheet-style display overrides.
7
+ */
8
+ export class SchematicSheetStyleResolver {
9
+ /**
10
+ * Resolves the displayed horizontal sheet-zone count after the page size
11
+ * has been normalized.
12
+ * @param {{ width: number, height: number, xZones: number, paperSize?: string, sheetStyle?: number }} sheet
13
+ * @returns {number}
14
+ */
15
+ static resolveXZones(sheet) {
16
+ const configuredXZones = Math.max(Number(sheet?.xZones || 0), 1)
17
+ const paperSize = String(sheet?.paperSize || '')
18
+ .trim()
19
+ .toUpperCase()
20
+
21
+ if (Number(sheet?.sheetStyle || 0) !== 1 && !paperSize) {
22
+ return configuredXZones
23
+ }
24
+
25
+ const width = Number(sheet?.width || 0)
26
+ const height = Number(sheet?.height || 0)
27
+ if (width < height) {
28
+ return configuredXZones
29
+ }
30
+
31
+ if (
32
+ paperSize === 'A2' ||
33
+ (width === 2339 && height === 1654) ||
34
+ paperSize === 'A3' ||
35
+ (width === 1654 && height === 1169)
36
+ ) {
37
+ return 8
38
+ }
39
+
40
+ if (paperSize === 'A4' || (width === 1169 && height === 827)) {
41
+ return 4
42
+ }
43
+
44
+ return configuredXZones
45
+ }
46
+ }