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,306 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ const ZOOM_IN_FACTOR = 0.97
6
+ const ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR
7
+ const MIN_SCALE_RATIO = 0.05
8
+ const MAX_SCALE_RATIO = 4
9
+
10
+ /**
11
+ * Adds wheel zoom and drag pan to rendered schematic and PCB SVGs.
12
+ */
13
+ export class SvgViewportController {
14
+ #svg
15
+ #defaultViewBox
16
+ #viewBox
17
+ #dragState
18
+ #boundWheel
19
+ #boundMouseDown
20
+ #boundMouseMove
21
+ #boundMouseUp
22
+
23
+ /**
24
+ * Creates an interactive viewport controller.
25
+ * @param {{ getAttribute: (name: string) => string | null, setAttribute: (name: string, value: string) => void, getBoundingClientRect: () => { left: number, top: number, width: number, height: number }, addEventListener: (type: string, listener: (event: any) => void, options?: any) => void, removeEventListener: (type: string, listener: (event: any) => void, options?: any) => void, classList?: { add: (...tokens: string[]) => void, remove: (...tokens: string[]) => void }, ownerDocument?: { addEventListener: (type: string, listener: (event: any) => void) => void, removeEventListener: (type: string, listener: (event: any) => void) => void, documentElement?: { classList?: { add: (...tokens: string[]) => void, remove: (...tokens: string[]) => void } } } }} svgElement
26
+ */
27
+ constructor(svgElement) {
28
+ this.#svg = svgElement
29
+ this.#defaultViewBox = this.#readViewBox()
30
+ this.#viewBox = { ...this.#defaultViewBox }
31
+ this.#dragState = null
32
+ this.#boundWheel = (event) => this.#handleWheel(event)
33
+ this.#boundMouseDown = (event) => this.#handleMouseDown(event)
34
+ this.#boundMouseMove = (event) => this.#handleMouseMove(event)
35
+ this.#boundMouseUp = (event) => this.#handleMouseUp(event)
36
+ this.#bindEvents()
37
+ this.#applyViewBox()
38
+ }
39
+
40
+ /**
41
+ * Removes listeners and clears active drag state.
42
+ * @returns {void}
43
+ */
44
+ dispose() {
45
+ this.#unbindEvents()
46
+ this.#stopDragging()
47
+ }
48
+
49
+ /**
50
+ * Binds browser events needed for SVG viewport interaction.
51
+ * @returns {void}
52
+ */
53
+ #bindEvents() {
54
+ this.#svg.addEventListener('wheel', this.#boundWheel, {
55
+ passive: false
56
+ })
57
+ this.#svg.addEventListener('mousedown', this.#boundMouseDown)
58
+ this.#getOwnerDocument().addEventListener(
59
+ 'mousemove',
60
+ this.#boundMouseMove
61
+ )
62
+ this.#getOwnerDocument().addEventListener('mouseup', this.#boundMouseUp)
63
+ }
64
+
65
+ /**
66
+ * Removes browser event listeners.
67
+ * @returns {void}
68
+ */
69
+ #unbindEvents() {
70
+ this.#svg.removeEventListener('wheel', this.#boundWheel, {
71
+ passive: false
72
+ })
73
+ this.#svg.removeEventListener('mousedown', this.#boundMouseDown)
74
+ this.#getOwnerDocument().removeEventListener(
75
+ 'mousemove',
76
+ this.#boundMouseMove
77
+ )
78
+ this.#getOwnerDocument().removeEventListener(
79
+ 'mouseup',
80
+ this.#boundMouseUp
81
+ )
82
+ }
83
+
84
+ /**
85
+ * Applies wheel zoom around the current cursor position.
86
+ * @param {{ deltaY?: number, clientX?: number, clientY?: number, preventDefault?: () => void }} event
87
+ * @returns {void}
88
+ */
89
+ #handleWheel(event) {
90
+ const deltaY = Number(event?.deltaY || 0)
91
+ if (deltaY === 0) return
92
+
93
+ const anchorPoint = this.#projectClientPointToDocument(
94
+ Number(event?.clientX || 0),
95
+ Number(event?.clientY || 0)
96
+ )
97
+ if (!anchorPoint) return
98
+
99
+ event?.preventDefault?.()
100
+
101
+ const zoomFactor = deltaY < 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR
102
+ const nextWidth = this.#clampWidth(this.#viewBox.width * zoomFactor)
103
+ const nextHeight = this.#clampHeight(this.#viewBox.height * zoomFactor)
104
+ const relativeX =
105
+ (anchorPoint.x - this.#viewBox.x) / this.#viewBox.width
106
+ const relativeY =
107
+ (anchorPoint.y - this.#viewBox.y) / this.#viewBox.height
108
+
109
+ this.#viewBox = {
110
+ x: anchorPoint.x - relativeX * nextWidth,
111
+ y: anchorPoint.y - relativeY * nextHeight,
112
+ width: nextWidth,
113
+ height: nextHeight
114
+ }
115
+
116
+ this.#applyViewBox()
117
+ }
118
+
119
+ /**
120
+ * Starts a drag pan on primary-button press.
121
+ * @param {{ button?: number, clientX?: number, clientY?: number, preventDefault?: () => void }} event
122
+ * @returns {void}
123
+ */
124
+ #handleMouseDown(event) {
125
+ if (Number(event?.button) !== 0) return
126
+
127
+ event?.preventDefault?.()
128
+ this.#lockDocumentScroll()
129
+ this.#dragState = {
130
+ startClientX: Number(event?.clientX || 0),
131
+ startClientY: Number(event?.clientY || 0),
132
+ originViewBox: { ...this.#viewBox }
133
+ }
134
+ this.#svg.classList?.add('is-panning')
135
+ }
136
+
137
+ /**
138
+ * Pans the viewBox while dragging.
139
+ * @param {{ buttons?: number, clientX?: number, clientY?: number, preventDefault?: () => void }} event
140
+ * @returns {void}
141
+ */
142
+ #handleMouseMove(event) {
143
+ if (!this.#dragState) return
144
+
145
+ if (Number(event?.buttons || 0) === 0) {
146
+ this.#stopDragging()
147
+ return
148
+ }
149
+
150
+ const rect = this.#svg.getBoundingClientRect()
151
+ if (rect.width <= 0 || rect.height <= 0) return
152
+
153
+ event?.preventDefault?.()
154
+
155
+ const deltaX =
156
+ ((Number(event?.clientX || 0) - this.#dragState.startClientX) /
157
+ rect.width) *
158
+ this.#dragState.originViewBox.width
159
+ const deltaY =
160
+ ((Number(event?.clientY || 0) - this.#dragState.startClientY) /
161
+ rect.height) *
162
+ this.#dragState.originViewBox.height
163
+
164
+ this.#viewBox = {
165
+ x: this.#dragState.originViewBox.x - deltaX,
166
+ y: this.#dragState.originViewBox.y - deltaY,
167
+ width: this.#dragState.originViewBox.width,
168
+ height: this.#dragState.originViewBox.height
169
+ }
170
+
171
+ this.#applyViewBox()
172
+ }
173
+
174
+ /**
175
+ * Stops a drag pan on primary-button release.
176
+ * @param {{ button?: number }} event
177
+ * @returns {void}
178
+ */
179
+ #handleMouseUp(event) {
180
+ if (Number(event?.button) !== 0) return
181
+ this.#stopDragging()
182
+ }
183
+
184
+ /**
185
+ * Clears the active drag state.
186
+ * @returns {void}
187
+ */
188
+ #stopDragging() {
189
+ this.#dragState = null
190
+ this.#svg.classList?.remove('is-panning')
191
+ this.#unlockDocumentScroll()
192
+ }
193
+
194
+ /**
195
+ * Returns the document-like target used for global move/up listeners.
196
+ * @returns {{ addEventListener: (type: string, listener: (event: any) => void) => void, removeEventListener: (type: string, listener: (event: any) => void) => void, documentElement?: { classList?: { add: (...tokens: string[]) => void, remove: (...tokens: string[]) => void } } }}
197
+ */
198
+ #getOwnerDocument() {
199
+ return this.#svg.ownerDocument || this.#svg
200
+ }
201
+
202
+ /**
203
+ * Locks page scrolling while a pan gesture is active.
204
+ * @returns {void}
205
+ */
206
+ #lockDocumentScroll() {
207
+ this.#getOwnerDocument().documentElement?.classList?.add(
208
+ 'is-svg-panning'
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Restores page scrolling after panning.
214
+ * @returns {void}
215
+ */
216
+ #unlockDocumentScroll() {
217
+ this.#getOwnerDocument().documentElement?.classList?.remove(
218
+ 'is-svg-panning'
219
+ )
220
+ }
221
+
222
+ /**
223
+ * Reads the SVG's current viewBox.
224
+ * @returns {{ x: number, y: number, width: number, height: number }}
225
+ */
226
+ #readViewBox() {
227
+ const rawValue = String(this.#svg.getAttribute('viewBox') || '')
228
+ const [x, y, width, height] = rawValue
229
+ .trim()
230
+ .split(/\s+/)
231
+ .map((value) => Number(value))
232
+
233
+ return {
234
+ x: Number.isFinite(x) ? x : 0,
235
+ y: Number.isFinite(y) ? y : 0,
236
+ width: Number.isFinite(width) && width > 0 ? width : 100,
237
+ height: Number.isFinite(height) && height > 0 ? height : 100
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Projects a client-space point into the current SVG document space.
243
+ * @param {number} clientX
244
+ * @param {number} clientY
245
+ * @returns {{ x: number, y: number } | null}
246
+ */
247
+ #projectClientPointToDocument(clientX, clientY) {
248
+ const rect = this.#svg.getBoundingClientRect()
249
+ if (rect.width <= 0 || rect.height <= 0) return null
250
+
251
+ const relativeX = (clientX - rect.left) / rect.width
252
+ const relativeY = (clientY - rect.top) / rect.height
253
+
254
+ return {
255
+ x: this.#viewBox.x + relativeX * this.#viewBox.width,
256
+ y: this.#viewBox.y + relativeY * this.#viewBox.height
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Clamps a candidate viewBox width to the allowed zoom range.
262
+ * @param {number} width
263
+ * @returns {number}
264
+ */
265
+ #clampWidth(width) {
266
+ const minimumWidth = this.#defaultViewBox.width * MIN_SCALE_RATIO
267
+ const maximumWidth = this.#defaultViewBox.width * MAX_SCALE_RATIO
268
+ return Math.min(Math.max(width, minimumWidth), maximumWidth)
269
+ }
270
+
271
+ /**
272
+ * Clamps a candidate viewBox height to the allowed zoom range.
273
+ * @param {number} height
274
+ * @returns {number}
275
+ */
276
+ #clampHeight(height) {
277
+ const minimumHeight = this.#defaultViewBox.height * MIN_SCALE_RATIO
278
+ const maximumHeight = this.#defaultViewBox.height * MAX_SCALE_RATIO
279
+ return Math.min(Math.max(height, minimumHeight), maximumHeight)
280
+ }
281
+
282
+ /**
283
+ * Writes the current viewBox to the SVG.
284
+ * @returns {void}
285
+ */
286
+ #applyViewBox() {
287
+ this.#svg.setAttribute(
288
+ 'viewBox',
289
+ [
290
+ SvgViewportController.#formatNumber(this.#viewBox.x),
291
+ SvgViewportController.#formatNumber(this.#viewBox.y),
292
+ SvgViewportController.#formatNumber(this.#viewBox.width),
293
+ SvgViewportController.#formatNumber(this.#viewBox.height)
294
+ ].join(' ')
295
+ )
296
+ }
297
+
298
+ /**
299
+ * Formats a numeric SVG value without noisy floating-point tails.
300
+ * @param {number} value
301
+ * @returns {string}
302
+ */
303
+ static #formatNumber(value) {
304
+ return String(Number(value.toFixed(4)))
305
+ }
306
+ }