altium-toolkit 1.0.7 → 1.0.9

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 (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,350 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Provides lightweight hit-test geometry helpers for PCB mil coordinates.
7
+ */
8
+ export class PcbInteractionGeometry {
9
+ /**
10
+ * Builds a circular geometry descriptor.
11
+ * @param {{ x?: unknown, y?: unknown }} center Center point.
12
+ * @param {unknown} radius Radius.
13
+ * @returns {object}
14
+ */
15
+ static circle(center, radius) {
16
+ return {
17
+ kind: 'circle',
18
+ center: PcbInteractionGeometry.point(center),
19
+ radius: Math.max(0, Number(radius) || 0)
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Builds a stroked segment geometry descriptor.
25
+ * @param {{ x?: unknown, y?: unknown }} start Start point.
26
+ * @param {{ x?: unknown, y?: unknown }} end End point.
27
+ * @param {unknown} radius Stroke radius.
28
+ * @returns {object}
29
+ */
30
+ static segment(start, end, radius = 0) {
31
+ return {
32
+ kind: 'segment',
33
+ start: PcbInteractionGeometry.point(start),
34
+ end: PcbInteractionGeometry.point(end),
35
+ radius: Math.max(0, Number(radius) || 0)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Builds a polygon geometry descriptor.
41
+ * @param {{ x?: unknown, y?: unknown }[]} points Polygon points.
42
+ * @returns {object}
43
+ */
44
+ static polygon(points) {
45
+ return {
46
+ kind: 'polygon',
47
+ points: (Array.isArray(points) ? points : []).map((point) =>
48
+ PcbInteractionGeometry.point(point)
49
+ )
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Builds an axis-aligned bounds geometry descriptor.
55
+ * @param {object} bounds Bounds.
56
+ * @returns {object}
57
+ */
58
+ static bounds(bounds) {
59
+ return {
60
+ kind: 'bounds',
61
+ bounds: PcbInteractionGeometry.normalizeBounds(bounds)
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Builds a rotated rectangle polygon geometry descriptor.
67
+ * @param {object} rectangle Rectangle.
68
+ * @returns {object}
69
+ */
70
+ static rotatedRectangle(rectangle) {
71
+ const center = PcbInteractionGeometry.point(rectangle)
72
+ const width = Math.max(0, Number(rectangle?.width) || 0)
73
+ const height = Math.max(0, Number(rectangle?.height) || 0)
74
+ const rotation = (Number(rectangle?.rotation) || 0) * (Math.PI / 180)
75
+ const cos = Math.cos(rotation)
76
+ const sin = Math.sin(rotation)
77
+ const halfWidth = width / 2
78
+ const halfHeight = height / 2
79
+
80
+ return PcbInteractionGeometry.polygon(
81
+ [
82
+ { x: -halfWidth, y: -halfHeight },
83
+ { x: halfWidth, y: -halfHeight },
84
+ { x: halfWidth, y: halfHeight },
85
+ { x: -halfWidth, y: halfHeight }
86
+ ].map((point) => ({
87
+ x: center.x + point.x * cos - point.y * sin,
88
+ y: center.y + point.x * sin + point.y * cos
89
+ }))
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Normalizes a point value.
95
+ * @param {{ x?: unknown, y?: unknown }} point Point-like value.
96
+ * @returns {{ x: number, y: number }}
97
+ */
98
+ static point(point) {
99
+ return {
100
+ x: Number(point?.x) || 0,
101
+ y: Number(point?.y) || 0
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Returns bounds for a geometry descriptor.
107
+ * @param {object | null | undefined} geometry Geometry descriptor.
108
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }}
109
+ */
110
+ static boundsFor(geometry) {
111
+ if (!geometry || typeof geometry !== 'object') {
112
+ return PcbInteractionGeometry.normalizeBounds(null)
113
+ }
114
+
115
+ if (geometry.kind === 'bounds') {
116
+ return PcbInteractionGeometry.normalizeBounds(geometry.bounds)
117
+ }
118
+
119
+ const points = PcbInteractionGeometry.#pointsForBounds(geometry)
120
+ if (!points.length) return PcbInteractionGeometry.normalizeBounds(null)
121
+
122
+ const xs = points.map((point) => point.x)
123
+ const ys = points.map((point) => point.y)
124
+ const minX = Math.min(...xs)
125
+ const minY = Math.min(...ys)
126
+ const maxX = Math.max(...xs)
127
+ const maxY = Math.max(...ys)
128
+
129
+ return {
130
+ minX,
131
+ minY,
132
+ maxX,
133
+ maxY,
134
+ width: maxX - minX,
135
+ height: maxY - minY
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Normalizes an axis-aligned bounds value.
141
+ * @param {object | null | undefined} bounds Bounds-like value.
142
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }}
143
+ */
144
+ static normalizeBounds(bounds) {
145
+ const minX = Number(bounds?.minX) || 0
146
+ const minY = Number(bounds?.minY) || 0
147
+ const width = Number(bounds?.widthMil ?? bounds?.width) || 0
148
+ const height = Number(bounds?.heightMil ?? bounds?.height) || 0
149
+ const maxX = Number.isFinite(Number(bounds?.maxX))
150
+ ? Number(bounds.maxX)
151
+ : minX + width
152
+ const maxY = Number.isFinite(Number(bounds?.maxY))
153
+ ? Number(bounds.maxY)
154
+ : minY + height
155
+
156
+ return {
157
+ minX: Math.min(minX, maxX),
158
+ minY: Math.min(minY, maxY),
159
+ maxX: Math.max(minX, maxX),
160
+ maxY: Math.max(minY, maxY),
161
+ width: Math.abs(maxX - minX),
162
+ height: Math.abs(maxY - minY)
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Returns whether a point intersects a geometry descriptor.
168
+ * @param {object | null | undefined} geometry Geometry descriptor.
169
+ * @param {{ x?: unknown, y?: unknown }} point Point-like value.
170
+ * @param {number} [tolerance] Extra tolerance.
171
+ * @returns {boolean}
172
+ */
173
+ static containsPoint(geometry, point, tolerance = 0) {
174
+ const normalizedPoint = PcbInteractionGeometry.point(point)
175
+ if (!geometry || typeof geometry !== 'object') return false
176
+
177
+ if (
178
+ !PcbInteractionGeometry.#boundsContainsPoint(
179
+ PcbInteractionGeometry.boundsFor(geometry),
180
+ normalizedPoint,
181
+ tolerance
182
+ )
183
+ ) {
184
+ return false
185
+ }
186
+
187
+ if (geometry.kind === 'circle') {
188
+ return (
189
+ PcbInteractionGeometry.#distance(
190
+ normalizedPoint,
191
+ geometry.center
192
+ ) <=
193
+ (Number(geometry.radius) || 0) + tolerance
194
+ )
195
+ }
196
+
197
+ if (geometry.kind === 'segment') {
198
+ return (
199
+ PcbInteractionGeometry.#pointToSegmentDistance(
200
+ normalizedPoint,
201
+ geometry.start,
202
+ geometry.end
203
+ ) <=
204
+ (Number(geometry.radius) || 0) + tolerance
205
+ )
206
+ }
207
+
208
+ if (geometry.kind === 'polygon') {
209
+ return PcbInteractionGeometry.#pointInPolygon(
210
+ normalizedPoint,
211
+ geometry.points || []
212
+ )
213
+ }
214
+
215
+ if (geometry.kind === 'bounds') {
216
+ return true
217
+ }
218
+
219
+ return false
220
+ }
221
+
222
+ /**
223
+ * Returns geometry points used for bounds calculations.
224
+ * @param {object} geometry Geometry descriptor.
225
+ * @returns {{ x: number, y: number }[]}
226
+ */
227
+ static #pointsForBounds(geometry) {
228
+ if (geometry.kind === 'circle') {
229
+ const center = PcbInteractionGeometry.point(geometry.center)
230
+ const radius = Math.max(0, Number(geometry.radius) || 0)
231
+ return [
232
+ { x: center.x - radius, y: center.y - radius },
233
+ { x: center.x + radius, y: center.y + radius }
234
+ ]
235
+ }
236
+
237
+ if (geometry.kind === 'segment') {
238
+ const start = PcbInteractionGeometry.point(geometry.start)
239
+ const end = PcbInteractionGeometry.point(geometry.end)
240
+ const radius = Math.max(0, Number(geometry.radius) || 0)
241
+ return [
242
+ {
243
+ x: Math.min(start.x, end.x) - radius,
244
+ y: Math.min(start.y, end.y) - radius
245
+ },
246
+ {
247
+ x: Math.max(start.x, end.x) + radius,
248
+ y: Math.max(start.y, end.y) + radius
249
+ }
250
+ ]
251
+ }
252
+
253
+ if (geometry.kind === 'polygon') {
254
+ return (Array.isArray(geometry.points) ? geometry.points : []).map(
255
+ (point) => PcbInteractionGeometry.point(point)
256
+ )
257
+ }
258
+
259
+ return []
260
+ }
261
+
262
+ /**
263
+ * Returns whether bounds contain a point.
264
+ * @param {object} bounds Bounds.
265
+ * @param {{ x: number, y: number }} point Point.
266
+ * @param {number} tolerance Tolerance.
267
+ * @returns {boolean}
268
+ */
269
+ static #boundsContainsPoint(bounds, point, tolerance) {
270
+ return (
271
+ point.x >= bounds.minX - tolerance &&
272
+ point.x <= bounds.maxX + tolerance &&
273
+ point.y >= bounds.minY - tolerance &&
274
+ point.y <= bounds.maxY + tolerance
275
+ )
276
+ }
277
+
278
+ /**
279
+ * Returns Euclidean distance between two points.
280
+ * @param {{ x?: unknown, y?: unknown }} first First point.
281
+ * @param {{ x?: unknown, y?: unknown }} second Second point.
282
+ * @returns {number}
283
+ */
284
+ static #distance(first, second) {
285
+ const a = PcbInteractionGeometry.point(first)
286
+ const b = PcbInteractionGeometry.point(second)
287
+ return Math.hypot(a.x - b.x, a.y - b.y)
288
+ }
289
+
290
+ /**
291
+ * Computes point-to-segment distance.
292
+ * @param {{ x: number, y: number }} point Point.
293
+ * @param {{ x?: unknown, y?: unknown }} start Segment start.
294
+ * @param {{ x?: unknown, y?: unknown }} end Segment end.
295
+ * @returns {number}
296
+ */
297
+ static #pointToSegmentDistance(point, start, end) {
298
+ const first = PcbInteractionGeometry.point(start)
299
+ const second = PcbInteractionGeometry.point(end)
300
+ const dx = second.x - first.x
301
+ const dy = second.y - first.y
302
+ const lengthSquared = dx * dx + dy * dy
303
+ if (lengthSquared === 0) {
304
+ return PcbInteractionGeometry.#distance(point, first)
305
+ }
306
+
307
+ const t = Math.max(
308
+ 0,
309
+ Math.min(
310
+ 1,
311
+ ((point.x - first.x) * dx + (point.y - first.y) * dy) /
312
+ lengthSquared
313
+ )
314
+ )
315
+ return PcbInteractionGeometry.#distance(point, {
316
+ x: first.x + t * dx,
317
+ y: first.y + t * dy
318
+ })
319
+ }
320
+
321
+ /**
322
+ * Returns whether a point is inside a polygon.
323
+ * @param {{ x: number, y: number }} point Point.
324
+ * @param {{ x?: unknown, y?: unknown }[]} polygon Polygon.
325
+ * @returns {boolean}
326
+ */
327
+ static #pointInPolygon(point, polygon) {
328
+ const points = (Array.isArray(polygon) ? polygon : []).map((entry) =>
329
+ PcbInteractionGeometry.point(entry)
330
+ )
331
+ let inside = false
332
+ for (
333
+ let index = 0, previous = points.length - 1;
334
+ index < points.length;
335
+ previous = index++
336
+ ) {
337
+ const current = points[index]
338
+ const before = points[previous]
339
+ const intersects =
340
+ current.y > point.y !== before.y > point.y &&
341
+ point.x <
342
+ ((before.x - current.x) * (point.y - current.y)) /
343
+ (before.y - current.y || Number.EPSILON) +
344
+ current.x
345
+ if (intersects) inside = !inside
346
+ }
347
+
348
+ return inside
349
+ }
350
+ }