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,398 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+ import { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+
8
+ const { escapeHtml, formatNumber, projectSchematicY } = SchematicSvgUtils
9
+
10
+ /**
11
+ * Renders normalized schematic shape primitives into SVG markup.
12
+ */
13
+ export class SchematicShapeRenderer {
14
+ /**
15
+ * Builds one schematic polygon primitive.
16
+ * @param {{ points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number }} polygon
17
+ * @param {number} sheetHeight
18
+ * @returns {string}
19
+ */
20
+ static buildPolygonMarkup(polygon, sheetHeight) {
21
+ if (
22
+ !polygon?.points?.length ||
23
+ polygon.transparent ||
24
+ !polygon.isSolid
25
+ ) {
26
+ return ''
27
+ }
28
+
29
+ return (
30
+ '<polygon class="schematic-polygon" points="' +
31
+ escapeHtml(
32
+ polygon.points
33
+ .map(
34
+ (point) =>
35
+ formatNumber(point.x) +
36
+ ',' +
37
+ formatNumber(
38
+ projectSchematicY(sheetHeight, point.y)
39
+ )
40
+ )
41
+ .join(' ')
42
+ ) +
43
+ '" fill="' +
44
+ escapeHtml(
45
+ SchematicColorResolver.resolveFill(
46
+ polygon.fill || 'none',
47
+ '--schematic-fill-color',
48
+ true
49
+ )
50
+ ) +
51
+ '" stroke="' +
52
+ escapeHtml(
53
+ SchematicColorResolver.resolveColor(
54
+ polygon.color,
55
+ '--schematic-default-ink-color'
56
+ )
57
+ ) +
58
+ '" stroke-width="' +
59
+ formatNumber(Math.max(polygon.lineWidth || 1, 0.8)) +
60
+ '"' +
61
+ SchematicShapeRenderer.#buildSchematicStrokeStyleAttributes(
62
+ polygon.lineWidth,
63
+ polygon.lineStyle
64
+ ) +
65
+ ' stroke-linejoin="round" />'
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Builds one schematic rectangle primitive.
71
+ * @param {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number }} rectangle
72
+ * @param {number} sheetHeight
73
+ * @returns {string}
74
+ */
75
+ static buildRectangleMarkup(rectangle, sheetHeight) {
76
+ return (
77
+ '<rect class="schematic-rectangle" x="' +
78
+ formatNumber(rectangle.x) +
79
+ '" y="' +
80
+ formatNumber(
81
+ projectSchematicY(sheetHeight, rectangle.y + rectangle.height)
82
+ ) +
83
+ '" width="' +
84
+ formatNumber(rectangle.width) +
85
+ '" height="' +
86
+ formatNumber(rectangle.height) +
87
+ '" fill="' +
88
+ escapeHtml(
89
+ SchematicColorResolver.resolveFill(
90
+ SchematicShapeRenderer.#resolveSchematicRectangleFill(
91
+ rectangle
92
+ ),
93
+ '--schematic-fill-color'
94
+ )
95
+ ) +
96
+ '" stroke="' +
97
+ escapeHtml(
98
+ SchematicColorResolver.resolveColor(
99
+ rectangle.color,
100
+ '--schematic-default-ink-color'
101
+ )
102
+ ) +
103
+ '" stroke-width="' +
104
+ formatNumber(Math.max(rectangle.lineWidth || 1, 0.8)) +
105
+ '"' +
106
+ SchematicShapeRenderer.#buildSchematicStrokeStyleAttributes(
107
+ rectangle.lineWidth,
108
+ rectangle.lineStyle
109
+ ) +
110
+ ' />'
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Builds one schematic arc primitive as an SVG path.
116
+ * Record-11 curves may supply `radiusY` for ellipse segments.
117
+ * @param {{ x: number, y: number, radius: number, radiusY?: number, startAngle: number, endAngle: number, color: string, width: number }} arc
118
+ * @param {number} sheetHeight
119
+ * @returns {string}
120
+ */
121
+ static buildArcMarkup(arc, sheetHeight) {
122
+ const radiusX = Math.max(Number(arc.radius) || 0, 0.8)
123
+ const radiusY = Math.max(Number(arc.radiusY ?? arc.radius) || 0, 0.8)
124
+ const delta = SchematicShapeRenderer.#normalizeArcDelta(
125
+ arc.startAngle,
126
+ arc.endAngle
127
+ )
128
+ const sweep = delta >= 0 ? 0 : 1
129
+ const path =
130
+ Math.abs(delta) >= 359.999
131
+ ? SchematicShapeRenderer.#buildFullCircleArcPath(
132
+ arc,
133
+ radiusX,
134
+ radiusY,
135
+ sheetHeight,
136
+ sweep
137
+ )
138
+ : SchematicShapeRenderer.#buildPartialArcPath(
139
+ arc,
140
+ radiusX,
141
+ radiusY,
142
+ sheetHeight,
143
+ delta,
144
+ sweep
145
+ )
146
+
147
+ return (
148
+ '<path class="schematic-arc" d="' +
149
+ path +
150
+ '" stroke="' +
151
+ escapeHtml(
152
+ SchematicColorResolver.resolveColor(
153
+ arc.color,
154
+ '--schematic-default-ink-color'
155
+ )
156
+ ) +
157
+ '" stroke-width="' +
158
+ formatNumber(Math.max(arc.width || 1, 0.8)) +
159
+ '" fill="none" />'
160
+ )
161
+ }
162
+
163
+ /**
164
+ * Builds one schematic ellipse primitive.
165
+ * @param {{ x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number }} ellipse
166
+ * @param {number} sheetHeight
167
+ * @returns {string}
168
+ */
169
+ static buildEllipseMarkup(ellipse, sheetHeight) {
170
+ return (
171
+ '<ellipse class="schematic-ellipse" cx="' +
172
+ formatNumber(ellipse.x) +
173
+ '" cy="' +
174
+ formatNumber(projectSchematicY(sheetHeight, ellipse.y)) +
175
+ '" rx="' +
176
+ formatNumber(Math.max(Number(ellipse.radiusX) || 0, 0.8)) +
177
+ '" ry="' +
178
+ formatNumber(Math.max(Number(ellipse.radiusY) || 0, 0.8)) +
179
+ '" fill="' +
180
+ escapeHtml(
181
+ SchematicColorResolver.resolveFill(
182
+ SchematicShapeRenderer.#resolveSchematicEllipseFill(
183
+ ellipse
184
+ ),
185
+ '--schematic-fill-light-color'
186
+ )
187
+ ) +
188
+ '" stroke="' +
189
+ escapeHtml(
190
+ SchematicColorResolver.resolveColor(
191
+ ellipse.color,
192
+ '--schematic-default-ink-color'
193
+ )
194
+ ) +
195
+ '" stroke-width="' +
196
+ formatNumber(Math.max(ellipse.lineWidth || 1, 0.8)) +
197
+ '" />'
198
+ )
199
+ }
200
+
201
+ /**
202
+ * Resolves the visible fill for one schematic rectangle primitive.
203
+ * @param {{ fill: string, isSolid: boolean, transparent: boolean }} rectangle
204
+ * @returns {string}
205
+ */
206
+ static #resolveSchematicRectangleFill(rectangle) {
207
+ if (rectangle.transparent || !rectangle.isSolid) {
208
+ return 'none'
209
+ }
210
+
211
+ return rectangle.fill || 'none'
212
+ }
213
+
214
+ /**
215
+ * Resolves the visible fill for one schematic ellipse primitive.
216
+ * @param {{ fill: string, isSolid: boolean, transparent: boolean }} ellipse
217
+ * @returns {string}
218
+ */
219
+ static #resolveSchematicEllipseFill(ellipse) {
220
+ if (ellipse.transparent || !ellipse.isSolid) {
221
+ return 'none'
222
+ }
223
+
224
+ return ellipse.fill || 'none'
225
+ }
226
+
227
+ /**
228
+ * Returns SVG stroke attributes for one schematic outline style.
229
+ * @param {number | undefined} lineWidth
230
+ * @param {number | undefined} lineStyle
231
+ * @returns {string}
232
+ */
233
+ static #buildSchematicStrokeStyleAttributes(lineWidth, lineStyle) {
234
+ if (Number(lineStyle || 0) !== 1) {
235
+ return ''
236
+ }
237
+
238
+ const dashLength = Math.max(Number(lineWidth || 1) * 8, 8)
239
+ const gapLength = Math.max(Number(lineWidth || 1) * 5, 5)
240
+
241
+ return (
242
+ ' stroke-dasharray="' +
243
+ formatNumber(dashLength) +
244
+ ' ' +
245
+ formatNumber(gapLength) +
246
+ '" stroke-linecap="round"'
247
+ )
248
+ }
249
+
250
+ /**
251
+ * Builds one non-circular SVG arc path.
252
+ * @param {{ x: number, y: number, radius: number, radiusY?: number, startAngle: number, endAngle: number }} arc
253
+ * @param {number} radiusX
254
+ * @param {number} radiusY
255
+ * @param {number} sheetHeight
256
+ * @param {number} delta
257
+ * @param {0 | 1} sweep
258
+ * @returns {string}
259
+ */
260
+ static #buildPartialArcPath(
261
+ arc,
262
+ radiusX,
263
+ radiusY,
264
+ sheetHeight,
265
+ delta,
266
+ sweep
267
+ ) {
268
+ const start = SchematicShapeRenderer.#projectArcPoint(
269
+ arc,
270
+ arc.startAngle,
271
+ sheetHeight,
272
+ radiusX,
273
+ radiusY
274
+ )
275
+ const end = SchematicShapeRenderer.#projectArcPoint(
276
+ arc,
277
+ arc.endAngle,
278
+ sheetHeight,
279
+ radiusX,
280
+ radiusY
281
+ )
282
+
283
+ return (
284
+ 'M ' +
285
+ formatNumber(start.x) +
286
+ ' ' +
287
+ formatNumber(start.y) +
288
+ ' A ' +
289
+ formatNumber(radiusX) +
290
+ ' ' +
291
+ formatNumber(radiusY) +
292
+ ' 0 ' +
293
+ (Math.abs(delta) > 180 ? '1' : '0') +
294
+ ' ' +
295
+ sweep +
296
+ ' ' +
297
+ formatNumber(end.x) +
298
+ ' ' +
299
+ formatNumber(end.y)
300
+ )
301
+ }
302
+
303
+ /**
304
+ * Builds one full-circle arc path from two half-arc segments.
305
+ * @param {{ x: number, y: number, startAngle: number }} arc
306
+ * @param {number} radiusX
307
+ * @param {number} radiusY
308
+ * @param {number} sheetHeight
309
+ * @param {0 | 1} sweep
310
+ * @returns {string}
311
+ */
312
+ static #buildFullCircleArcPath(arc, radiusX, radiusY, sheetHeight, sweep) {
313
+ const startAngle = Number(arc.startAngle) || 0
314
+ const midAngle = startAngle + (sweep === 0 ? 180 : -180)
315
+ const start = SchematicShapeRenderer.#projectArcPoint(
316
+ arc,
317
+ startAngle,
318
+ sheetHeight,
319
+ radiusX,
320
+ radiusY
321
+ )
322
+ const mid = SchematicShapeRenderer.#projectArcPoint(
323
+ arc,
324
+ midAngle,
325
+ sheetHeight,
326
+ radiusX,
327
+ radiusY
328
+ )
329
+
330
+ return (
331
+ 'M ' +
332
+ formatNumber(start.x) +
333
+ ' ' +
334
+ formatNumber(start.y) +
335
+ ' A ' +
336
+ formatNumber(radiusX) +
337
+ ' ' +
338
+ formatNumber(radiusY) +
339
+ ' 0 0 ' +
340
+ sweep +
341
+ ' ' +
342
+ formatNumber(mid.x) +
343
+ ' ' +
344
+ formatNumber(mid.y) +
345
+ ' A ' +
346
+ formatNumber(radiusX) +
347
+ ' ' +
348
+ formatNumber(radiusY) +
349
+ ' 0 0 ' +
350
+ sweep +
351
+ ' ' +
352
+ formatNumber(start.x) +
353
+ ' ' +
354
+ formatNumber(start.y)
355
+ )
356
+ }
357
+
358
+ /**
359
+ * Projects one schematic arc point into the SVG coordinate system.
360
+ * @param {{ x: number, y: number }} arc
361
+ * @param {number} angle
362
+ * @param {number} sheetHeight
363
+ * @param {number} radiusX
364
+ * @param {number} radiusY
365
+ * @returns {{ x: number, y: number }}
366
+ */
367
+ static #projectArcPoint(arc, angle, sheetHeight, radiusX, radiusY) {
368
+ const radians = (Number(angle) * Math.PI) / 180
369
+
370
+ return {
371
+ x: Number(arc.x) + radiusX * Math.cos(radians),
372
+ y: projectSchematicY(
373
+ sheetHeight,
374
+ Number(arc.y) + radiusY * Math.sin(radians)
375
+ )
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Keeps one schematic arc delta inside a single turn.
381
+ * @param {number} startAngle
382
+ * @param {number} endAngle
383
+ * @returns {number}
384
+ */
385
+ static #normalizeArcDelta(startAngle, endAngle) {
386
+ let delta = Number(endAngle) - Number(startAngle)
387
+
388
+ while (delta <= -360) {
389
+ delta += 360
390
+ }
391
+
392
+ while (delta > 360) {
393
+ delta -= 360
394
+ }
395
+
396
+ return delta
397
+ }
398
+ }