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,351 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds a normalized single-sheet schematic net model from recovered
7
+ * geometry and named connectivity markers.
8
+ */
9
+ export class SchematicNetlistBuilder {
10
+ /**
11
+ * Builds normalized nets and connectivity diagnostics.
12
+ * @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[] }} schematic
13
+ * @returns {{ nets: { name: string, segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
14
+ */
15
+ static build(schematic) {
16
+ const diagnostics = []
17
+ const segments = (schematic.lines || []).filter(
18
+ (line) => !line.ownerIndex && line.isBus !== true
19
+ )
20
+
21
+ if (!segments.length) {
22
+ return { nets: [], diagnostics }
23
+ }
24
+
25
+ const groups = SchematicNetlistBuilder.#groupWireSegments(
26
+ segments,
27
+ schematic.junctions || []
28
+ )
29
+ let unknownNetIndex = 0
30
+ const nets = groups.map((group) => {
31
+ const labels = (schematic.texts || []).filter(
32
+ (text) =>
33
+ text.recordType === '25' &&
34
+ SchematicNetlistBuilder.#groupContainsPoint(group, text)
35
+ )
36
+ const powerPorts = (schematic.texts || []).filter(
37
+ (text) =>
38
+ text.recordType === '17' &&
39
+ SchematicNetlistBuilder.#groupContainsPoint(group, text)
40
+ )
41
+ const pins = (schematic.pins || []).filter((pin) =>
42
+ SchematicNetlistBuilder.#groupContainsPoint(
43
+ group,
44
+ SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
45
+ )
46
+ )
47
+ const ports = (schematic.ports || []).filter((port) =>
48
+ SchematicNetlistBuilder.#groupContainsPoint(
49
+ group,
50
+ SchematicNetlistBuilder.#resolvePortConnectionPoint(port)
51
+ )
52
+ )
53
+ const junctions = (schematic.junctions || []).filter((junction) =>
54
+ SchematicNetlistBuilder.#groupContainsPoint(group, junction)
55
+ )
56
+ const busEntries = (schematic.busEntries || []).filter((busEntry) =>
57
+ group.some(
58
+ (segment) =>
59
+ SchematicNetlistBuilder.#lineContainsPoint(segment, {
60
+ x: busEntry.x1,
61
+ y: busEntry.y1
62
+ }) ||
63
+ SchematicNetlistBuilder.#lineContainsPoint(segment, {
64
+ x: busEntry.x2,
65
+ y: busEntry.y2
66
+ })
67
+ )
68
+ )
69
+ const sheetEntries = (schematic.sheetEntries || []).filter(
70
+ (sheetEntry) =>
71
+ SchematicNetlistBuilder.#groupContainsPoint(
72
+ group,
73
+ sheetEntry
74
+ )
75
+ )
76
+ const explicitNames = [
77
+ ...new Set(
78
+ [
79
+ ...powerPorts.map((item) => item.text),
80
+ ...labels.map((item) => item.text)
81
+ ].filter(Boolean)
82
+ )
83
+ ]
84
+ const name =
85
+ explicitNames[0] || 'UnknownNet' + String(unknownNetIndex++)
86
+
87
+ if (explicitNames.length > 1) {
88
+ diagnostics.push({
89
+ severity: 'warning',
90
+ message:
91
+ 'Multiple explicit net names were recovered for one schematic net: ' +
92
+ explicitNames.join(', ') +
93
+ '.'
94
+ })
95
+ }
96
+
97
+ return {
98
+ name,
99
+ segments: group,
100
+ labels,
101
+ powerPorts,
102
+ pins,
103
+ ports,
104
+ junctions,
105
+ busEntries,
106
+ sheetEntries
107
+ }
108
+ })
109
+
110
+ return { nets, diagnostics }
111
+ }
112
+
113
+ /**
114
+ * Groups wire segments by direct endpoint contact or junction-mediated tee
115
+ * contact.
116
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} segments
117
+ * @param {{ x: number, y: number }[]} junctions
118
+ * @returns {{ x1: number, y1: number, x2: number, y2: number }[][]}
119
+ */
120
+ static #groupWireSegments(segments, junctions) {
121
+ const parents = segments.map((_, index) => index)
122
+
123
+ for (let leftIndex = 0; leftIndex < segments.length; leftIndex += 1) {
124
+ for (
125
+ let rightIndex = leftIndex + 1;
126
+ rightIndex < segments.length;
127
+ rightIndex += 1
128
+ ) {
129
+ if (
130
+ SchematicNetlistBuilder.#segmentsAreConnected(
131
+ segments[leftIndex],
132
+ segments[rightIndex],
133
+ junctions
134
+ )
135
+ ) {
136
+ SchematicNetlistBuilder.#union(
137
+ parents,
138
+ leftIndex,
139
+ rightIndex
140
+ )
141
+ }
142
+ }
143
+ }
144
+
145
+ const groups = new Map()
146
+
147
+ for (let index = 0; index < segments.length; index += 1) {
148
+ const root = SchematicNetlistBuilder.#find(parents, index)
149
+
150
+ if (!groups.has(root)) {
151
+ groups.set(root, [])
152
+ }
153
+
154
+ groups.get(root).push(segments[index])
155
+ }
156
+
157
+ return [...groups.values()].sort((left, right) => {
158
+ const leftMinX = Math.min(
159
+ ...left.map((segment) => Math.min(segment.x1, segment.x2))
160
+ )
161
+ const leftMinY = Math.min(
162
+ ...left.map((segment) => Math.min(segment.y1, segment.y2))
163
+ )
164
+ const rightMinX = Math.min(
165
+ ...right.map((segment) => Math.min(segment.x1, segment.x2))
166
+ )
167
+ const rightMinY = Math.min(
168
+ ...right.map((segment) => Math.min(segment.y1, segment.y2))
169
+ )
170
+
171
+ return leftMinY - rightMinY || leftMinX - rightMinX
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Returns true when two segments share connectivity.
177
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} left
178
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} right
179
+ * @param {{ x: number, y: number }[]} junctions
180
+ * @returns {boolean}
181
+ */
182
+ static #segmentsAreConnected(left, right, junctions) {
183
+ const leftEndpoints = [
184
+ { x: left.x1, y: left.y1 },
185
+ { x: left.x2, y: left.y2 }
186
+ ]
187
+ const rightEndpoints = [
188
+ { x: right.x1, y: right.y1 },
189
+ { x: right.x2, y: right.y2 }
190
+ ]
191
+
192
+ if (
193
+ leftEndpoints.some((leftPoint) =>
194
+ rightEndpoints.some((rightPoint) =>
195
+ SchematicNetlistBuilder.#pointsEqual(leftPoint, rightPoint)
196
+ )
197
+ )
198
+ ) {
199
+ return true
200
+ }
201
+
202
+ for (const point of leftEndpoints) {
203
+ if (
204
+ SchematicNetlistBuilder.#lineContainsPoint(right, point) &&
205
+ junctions.some((junction) =>
206
+ SchematicNetlistBuilder.#pointsEqual(junction, point)
207
+ )
208
+ ) {
209
+ return true
210
+ }
211
+ }
212
+
213
+ for (const point of rightEndpoints) {
214
+ if (
215
+ SchematicNetlistBuilder.#lineContainsPoint(left, point) &&
216
+ junctions.some((junction) =>
217
+ SchematicNetlistBuilder.#pointsEqual(junction, point)
218
+ )
219
+ ) {
220
+ return true
221
+ }
222
+ }
223
+
224
+ return false
225
+ }
226
+
227
+ /**
228
+ * Returns true when any segment in one group contains the candidate point.
229
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} group
230
+ * @param {{ x: number, y: number }} point
231
+ * @returns {boolean}
232
+ */
233
+ static #groupContainsPoint(group, point) {
234
+ return group.some((segment) =>
235
+ SchematicNetlistBuilder.#lineContainsPoint(segment, point)
236
+ )
237
+ }
238
+
239
+ /**
240
+ * Returns true when one point lies on the segment, including endpoints.
241
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} line
242
+ * @param {{ x: number, y: number }} point
243
+ * @returns {boolean}
244
+ */
245
+ static #lineContainsPoint(line, point) {
246
+ const tolerance = 0.01
247
+ const dx = Number(line.x2) - Number(line.x1)
248
+ const dy = Number(line.y2) - Number(line.y1)
249
+ const cross =
250
+ (Number(point.y) - Number(line.y1)) * dx -
251
+ (Number(point.x) - Number(line.x1)) * dy
252
+
253
+ if (Math.abs(cross) > tolerance) {
254
+ return false
255
+ }
256
+
257
+ const minX = Math.min(Number(line.x1), Number(line.x2)) - tolerance
258
+ const maxX = Math.max(Number(line.x1), Number(line.x2)) + tolerance
259
+ const minY = Math.min(Number(line.y1), Number(line.y2)) - tolerance
260
+ const maxY = Math.max(Number(line.y1), Number(line.y2)) + tolerance
261
+
262
+ return (
263
+ Number(point.x) >= minX &&
264
+ Number(point.x) <= maxX &&
265
+ Number(point.y) >= minY &&
266
+ Number(point.y) <= maxY
267
+ )
268
+ }
269
+
270
+ /**
271
+ * Resolves the wire-connection point for one normalized pin.
272
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
273
+ * @returns {{ x: number, y: number }}
274
+ */
275
+ static #resolvePinConnectionPoint(pin) {
276
+ switch (pin.orientation) {
277
+ case 'right':
278
+ return { x: pin.x + pin.length, y: pin.y }
279
+ case 'top':
280
+ return { x: pin.x, y: pin.y + pin.length }
281
+ case 'bottom':
282
+ return { x: pin.x, y: pin.y - pin.length }
283
+ case 'left':
284
+ default:
285
+ return { x: pin.x - pin.length, y: pin.y }
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Resolves the wire-connection point for one normalized off-sheet port.
291
+ * @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }} port
292
+ * @returns {{ x: number, y: number }}
293
+ */
294
+ static #resolvePortConnectionPoint(port) {
295
+ switch (port.direction) {
296
+ case 'right':
297
+ return { x: port.x + port.width, y: port.y }
298
+ case 'up':
299
+ return { x: port.x, y: port.y + port.width }
300
+ case 'down':
301
+ return { x: port.x, y: port.y }
302
+ case 'left':
303
+ default:
304
+ return { x: port.x, y: port.y }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Returns true when two points share the same schematic location.
310
+ * @param {{ x: number, y: number }} left
311
+ * @param {{ x: number, y: number }} right
312
+ * @returns {boolean}
313
+ */
314
+ static #pointsEqual(left, right) {
315
+ return (
316
+ Math.abs(Number(left.x) - Number(right.x)) <= 0.01 &&
317
+ Math.abs(Number(left.y) - Number(right.y)) <= 0.01
318
+ )
319
+ }
320
+
321
+ /**
322
+ * Finds one union-find root.
323
+ * @param {number[]} parents
324
+ * @param {number} index
325
+ * @returns {number}
326
+ */
327
+ static #find(parents, index) {
328
+ if (parents[index] === index) {
329
+ return index
330
+ }
331
+
332
+ parents[index] = SchematicNetlistBuilder.#find(parents, parents[index])
333
+
334
+ return parents[index]
335
+ }
336
+
337
+ /**
338
+ * Unions two union-find roots.
339
+ * @param {number[]} parents
340
+ * @param {number} leftIndex
341
+ * @param {number} rightIndex
342
+ */
343
+ static #union(parents, leftIndex, rightIndex) {
344
+ const leftRoot = SchematicNetlistBuilder.#find(parents, leftIndex)
345
+ const rightRoot = SchematicNetlistBuilder.#find(parents, rightIndex)
346
+
347
+ if (leftRoot !== rightRoot) {
348
+ parents[rightRoot] = leftRoot
349
+ }
350
+ }
351
+ }