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,182 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Shared SVG and markup formatting helpers.
7
+ */
8
+ export class SchematicSvgUtils {
9
+ /**
10
+ * Escapes user-facing markup.
11
+ * @param {string} value
12
+ * @returns {string}
13
+ */
14
+ static escapeHtml(value) {
15
+ return String(value)
16
+ .replaceAll('&', '&')
17
+ .replaceAll('<', '&lt;')
18
+ .replaceAll('>', '&gt;')
19
+ .replaceAll('"', '&quot;')
20
+ }
21
+
22
+ /**
23
+ * Formats a concise numeric attribute.
24
+ * @param {number} value
25
+ * @returns {string}
26
+ */
27
+ static formatNumber(value) {
28
+ return Number(value).toFixed(2).replace(/\.00$/, '')
29
+ }
30
+
31
+ /**
32
+ * Converts bottom-left schematic coordinates into SVG coordinates.
33
+ * @param {number} sheetHeight
34
+ * @param {number} value
35
+ * @returns {number}
36
+ */
37
+ static projectSchematicY(sheetHeight, value) {
38
+ return Number(sheetHeight) - Number(value)
39
+ }
40
+
41
+ /**
42
+ * Creates one escaped SVG text element.
43
+ * @param {string} className
44
+ * @param {number} x
45
+ * @param {number} y
46
+ * @param {string} text
47
+ * @param {string} color
48
+ * @param {'start' | 'end' | 'middle'} anchor
49
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, segments?: { text: string, overline?: boolean }[] }} [options]
50
+ * @returns {string}
51
+ */
52
+ static createSvgText(className, x, y, text, color, anchor, options = {}) {
53
+ const hasSegments = Array.isArray(options.segments)
54
+ ? options.segments.some((segment) => segment?.text)
55
+ : false
56
+ const hasPlainText =
57
+ text !== undefined && text !== null && String(text).length > 0
58
+ if (!hasSegments && !hasPlainText) return ''
59
+
60
+ return (
61
+ '<text class="' +
62
+ SchematicSvgUtils.escapeHtml(className) +
63
+ '" x="' +
64
+ SchematicSvgUtils.formatNumber(x) +
65
+ '" y="' +
66
+ SchematicSvgUtils.formatNumber(y) +
67
+ '" fill="' +
68
+ SchematicSvgUtils.escapeHtml(color) +
69
+ '" text-anchor="' +
70
+ SchematicSvgUtils.escapeHtml(anchor) +
71
+ '"' +
72
+ SchematicSvgUtils.#buildSvgTextStyleAttributes(x, y, options) +
73
+ '>' +
74
+ SchematicSvgUtils.#buildSvgTextContent(text, options.segments) +
75
+ '</text>'
76
+ )
77
+ }
78
+
79
+ /**
80
+ * Returns only the trailing file segment for footer display.
81
+ * @param {string | undefined} fileName
82
+ * @returns {string}
83
+ */
84
+ static basename(fileName) {
85
+ if (!fileName) return ''
86
+ const parts = String(fileName).split(/[\\/]/)
87
+ return parts.at(-1) || ''
88
+ }
89
+
90
+ /**
91
+ * Formats the current date like Altium's default title block.
92
+ * @returns {string}
93
+ */
94
+ static buildCurrentDateValue() {
95
+ const today = new Date()
96
+ const month = String(today.getMonth() + 1)
97
+ const day = String(today.getDate()).padStart(2, '0')
98
+ return month + '/' + day + '/' + today.getFullYear()
99
+ }
100
+
101
+ /**
102
+ * Creates optional inline SVG text attributes for typography and rotation.
103
+ * @param {number} x
104
+ * @param {number} y
105
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }} options
106
+ * @returns {string}
107
+ */
108
+ static #buildSvgTextStyleAttributes(x, y, options) {
109
+ let attributes = ''
110
+
111
+ if (options.fontSize) {
112
+ attributes +=
113
+ ' font-size="' +
114
+ SchematicSvgUtils.escapeHtml(
115
+ SchematicSvgUtils.formatNumber(options.fontSize)
116
+ ) +
117
+ '"'
118
+ }
119
+
120
+ if (options.fontFamily) {
121
+ attributes +=
122
+ ' font-family="' +
123
+ SchematicSvgUtils.escapeHtml(options.fontFamily) +
124
+ '"'
125
+ }
126
+
127
+ if (options.fontWeight) {
128
+ attributes +=
129
+ ' font-weight="' +
130
+ SchematicSvgUtils.escapeHtml(String(options.fontWeight)) +
131
+ '"'
132
+ }
133
+
134
+ if (options.rotation) {
135
+ attributes +=
136
+ ' transform="rotate(' +
137
+ SchematicSvgUtils.escapeHtml(
138
+ SchematicSvgUtils.formatNumber(options.rotation)
139
+ ) +
140
+ ' ' +
141
+ SchematicSvgUtils.escapeHtml(
142
+ SchematicSvgUtils.formatNumber(x)
143
+ ) +
144
+ ' ' +
145
+ SchematicSvgUtils.escapeHtml(
146
+ SchematicSvgUtils.formatNumber(y)
147
+ ) +
148
+ ')"'
149
+ }
150
+
151
+ return attributes
152
+ }
153
+
154
+ /**
155
+ * Builds escaped text or segmented tspan markup for one SVG text element.
156
+ * @param {string} text
157
+ * @param {{ text: string, overline?: boolean }[] | undefined} segments
158
+ * @returns {string}
159
+ */
160
+ static #buildSvgTextContent(text, segments) {
161
+ if (
162
+ Array.isArray(segments) &&
163
+ segments.some((segment) => segment?.text)
164
+ ) {
165
+ return segments
166
+ .filter((segment) => segment?.text)
167
+ .map(
168
+ (segment) =>
169
+ '<tspan text-decoration="' +
170
+ SchematicSvgUtils.escapeHtml(
171
+ segment.overline ? 'overline' : 'none'
172
+ ) +
173
+ '">' +
174
+ SchematicSvgUtils.escapeHtml(segment.text) +
175
+ '</tspan>'
176
+ )
177
+ .join('')
178
+ }
179
+
180
+ return SchematicSvgUtils.escapeHtml(text)
181
+ }
182
+ }
@@ -0,0 +1,204 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Shared typography helpers for synthetic schematic SVG labels.
7
+ */
8
+ export class SchematicTypography {
9
+ /**
10
+ * Returns true when the schematic already contains a visible designator text
11
+ * close to one component origin.
12
+ * @param {{ x?: number, y?: number }} component
13
+ * @param {{ x: number, y: number, name?: string }[]} texts
14
+ * @returns {boolean}
15
+ */
16
+ static hasNearbyVisibleDesignatorText(component, texts) {
17
+ return texts.some(
18
+ (text) =>
19
+ String(text.name || '')
20
+ .trim()
21
+ .toLowerCase() === 'designator' &&
22
+ Math.abs(Number(text.x) - Number(component.x)) <= 80 &&
23
+ Math.abs(Number(text.y) - Number(component.y)) <= 80
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Builds the default font options used for synthetic schematic labels.
29
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
30
+ * @returns {{ fontSize: number, fontFamily: string, fontWeight: number }}
31
+ */
32
+ static buildDefaultSchematicFontOptions(sheet) {
33
+ const font = sheet?.fonts?.['1'] || {
34
+ size: 10,
35
+ family: 'Times New Roman',
36
+ bold: false
37
+ }
38
+
39
+ return {
40
+ fontSize: SchematicTypography.#toSvgFontSize(font.size),
41
+ fontFamily: font.family || 'Times New Roman',
42
+ fontWeight: font.bold ? 700 : 400
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Builds default font options with the viewer-wide one-point reduction
48
+ * already applied.
49
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
50
+ * @returns {{ fontSize: number | undefined, fontFamily: string, fontWeight: number }}
51
+ */
52
+ static buildViewerSchematicFontOptions(sheet) {
53
+ return SchematicTypography.withViewerFontSize(
54
+ SchematicTypography.buildDefaultSchematicFontOptions(sheet)
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Builds render options for one schematic text label, including the signed
60
+ * SVG rotation derived from the original Altium orientation and mirrored
61
+ * source state.
62
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text
63
+ * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }}
64
+ */
65
+ static buildSchematicTextRenderOptions(text) {
66
+ return {
67
+ fontSize: SchematicTypography.resolveViewerFontSize(text.fontSize),
68
+ fontFamily: text.fontFamily,
69
+ fontWeight: text.fontWeight,
70
+ rotation: SchematicTypography.#resolveSignedTextRotation(
71
+ text.rotation,
72
+ text.sourceOrientation,
73
+ text.isMirrored
74
+ )
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Applies the viewer-wide one-point text reduction to one option bag.
80
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }} options
81
+ * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }}
82
+ */
83
+ static withViewerFontSize(options) {
84
+ return {
85
+ ...options,
86
+ fontSize: SchematicTypography.resolveViewerFontSize(
87
+ options?.fontSize
88
+ )
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Returns the one-point-smaller font size used for viewer text.
94
+ * @param {number | undefined} size
95
+ * @returns {number | undefined}
96
+ */
97
+ static resolveViewerFontSize(size) {
98
+ const numericSize = Number(size)
99
+
100
+ if (!Number.isFinite(numericSize) || numericSize <= 0) {
101
+ return undefined
102
+ }
103
+
104
+ return Math.max(numericSize - 1, 1)
105
+ }
106
+
107
+ /**
108
+ * Collects number-only owner groups whose top pin numbers should rotate
109
+ * along the vertical pin axis.
110
+ * @param {{ ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
111
+ * @returns {Set<string>}
112
+ */
113
+ static collectRotatedVerticalNumberOwners(pins) {
114
+ const ownerPins = new Map()
115
+
116
+ for (const pin of pins) {
117
+ const ownerIndex = String(pin.ownerIndex || '').trim()
118
+ if (!ownerIndex) continue
119
+ if (!ownerPins.has(ownerIndex)) ownerPins.set(ownerIndex, [])
120
+ ownerPins.get(ownerIndex).push(pin)
121
+ }
122
+
123
+ return new Set(
124
+ [...ownerPins.entries()]
125
+ .filter(([, groupedPins]) => {
126
+ const hasTopPin = groupedPins.some(
127
+ (pin) => pin.orientation === 'top'
128
+ )
129
+ const hasHorizontalPins = groupedPins.some(
130
+ (pin) =>
131
+ pin.orientation === 'left' ||
132
+ pin.orientation === 'right'
133
+ )
134
+ return (
135
+ groupedPins.length >= 4 &&
136
+ hasTopPin &&
137
+ hasHorizontalPins &&
138
+ groupedPins.every(
139
+ (pin) =>
140
+ (pin.labelMode || 'name-and-number') ===
141
+ 'number-only'
142
+ )
143
+ )
144
+ })
145
+ .map(([ownerIndex]) => ownerIndex)
146
+ )
147
+ }
148
+
149
+ /**
150
+ * Collects owner/text pairs that already expose explicit pin-name labels as
151
+ * free text primitives, so the pin renderer can avoid duplicating them.
152
+ * @param {{ ownerIndex?: string, recordType?: string, text?: string }[]} texts
153
+ * @returns {Set<string>}
154
+ */
155
+ static collectExplicitOwnerPinNameLabels(texts) {
156
+ return new Set(
157
+ texts
158
+ .filter(
159
+ (text) =>
160
+ text &&
161
+ text.recordType === '4' &&
162
+ String(text.ownerIndex || '').trim() &&
163
+ String(text.text || '').trim()
164
+ )
165
+ .map(
166
+ (text) =>
167
+ String(text.ownerIndex || '').trim() +
168
+ '::' +
169
+ String(text.text || '').trim()
170
+ )
171
+ )
172
+ }
173
+
174
+ /**
175
+ * Converts Altium point sizes into SVG font units.
176
+ * @param {number} size
177
+ * @returns {number}
178
+ */
179
+ static #toSvgFontSize(size) {
180
+ return Number(size || 10)
181
+ }
182
+
183
+ /**
184
+ * Resolves the signed SVG rotation for one schematic text label.
185
+ * @param {number | undefined} rotation
186
+ * @param {number | undefined} sourceOrientation
187
+ * @param {boolean | undefined} isMirrored
188
+ * @returns {number}
189
+ */
190
+ static #resolveSignedTextRotation(rotation, sourceOrientation, isMirrored) {
191
+ const normalizedRotation = Number(rotation || 0)
192
+
193
+ if (!normalizedRotation) {
194
+ return 0
195
+ }
196
+
197
+ const signedRotation =
198
+ Number(sourceOrientation || 0) === 3
199
+ ? normalizedRotation
200
+ : -normalizedRotation
201
+
202
+ return isMirrored ? -signedRotation : signedRotation
203
+ }
204
+ }
@@ -0,0 +1,29 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { AltiumParser } from '../core/altium/AltiumParser.mjs'
6
+
7
+ self.addEventListener('message', (event) => {
8
+ const payload = event?.data || {}
9
+ if (payload.type !== 'parse:file') return
10
+
11
+ try {
12
+ const documentModel = AltiumParser.parseArrayBuffer(
13
+ String(payload.fileName || 'document'),
14
+ payload.buffer
15
+ )
16
+ self.postMessage({
17
+ type: 'parser:success',
18
+ requestId: String(payload.requestId || ''),
19
+ documentModel
20
+ })
21
+ } catch (error) {
22
+ self.postMessage({
23
+ type: 'parser:error',
24
+ requestId: String(payload.requestId || ''),
25
+ message:
26
+ error instanceof Error ? error.message : 'Parser worker failed.'
27
+ })
28
+ }
29
+ })