altium-toolkit 1.0.8 → 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 (88) 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/styles/altium-renderers.css +19 -0
  77. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  78. package/src/ui/PcbInteractionIndex.mjs +9 -4
  79. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  80. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  81. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  82. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  83. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  84. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  85. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  86. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  87. package/src/ui/SchematicTypography.mjs +48 -5
  88. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,271 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds deterministic read-only schematic connectivity QA findings.
7
+ */
8
+ export class SchematicConnectivityQaBuilder {
9
+ static SCHEMA_ID = 'altium-toolkit.schematic.connectivity-qa.a1'
10
+
11
+ /**
12
+ * Builds connectivity QA from the normalized single-sheet net model.
13
+ * @param {{ nets?: object[], texts?: object[], pins?: object[], ports?: object[], junctions?: object[] }} schematic Normalized schematic fragments.
14
+ * @returns {object}
15
+ */
16
+ static build(schematic) {
17
+ const nets = Array.isArray(schematic?.nets) ? schematic.nets : []
18
+ const labels = (schematic?.texts || []).filter(
19
+ (text) => text.recordType === '25'
20
+ )
21
+ const ports = Array.isArray(schematic?.ports) ? schematic.ports : []
22
+ const pins = Array.isArray(schematic?.pins) ? schematic.pins : []
23
+ const junctions = Array.isArray(schematic?.junctions)
24
+ ? schematic.junctions
25
+ : []
26
+ const findings = [
27
+ ...SchematicConnectivityQaBuilder.#implicitNetFindings(nets),
28
+ ...SchematicConnectivityQaBuilder.#danglingLabelFindings(
29
+ labels,
30
+ nets
31
+ ),
32
+ ...SchematicConnectivityQaBuilder.#orphanPortFindings(ports, nets),
33
+ ...SchematicConnectivityQaBuilder.#unconnectedPinFindings(
34
+ pins,
35
+ nets
36
+ ),
37
+ ...SchematicConnectivityQaBuilder.#ambiguousJunctionFindings(
38
+ junctions,
39
+ nets
40
+ )
41
+ ]
42
+
43
+ return {
44
+ schema: SchematicConnectivityQaBuilder.SCHEMA_ID,
45
+ summary: SchematicConnectivityQaBuilder.#summary(nets, findings),
46
+ findings
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Reports nets that received a synthesized name.
52
+ * @param {object[]} nets Normalized nets.
53
+ * @returns {object[]}
54
+ */
55
+ static #implicitNetFindings(nets) {
56
+ return nets
57
+ .filter((net) => /^UnknownNet\d+$/u.test(String(net.name || '')))
58
+ .map((net) => ({
59
+ code: 'schematic.connectivity.implicit-net-name',
60
+ severity: 'info',
61
+ netName: net.name,
62
+ segmentCount: (net.segments || []).length
63
+ }))
64
+ }
65
+
66
+ /**
67
+ * Reports wire labels not assigned to any net.
68
+ * @param {object[]} labels Wire labels.
69
+ * @param {object[]} nets Normalized nets.
70
+ * @returns {object[]}
71
+ */
72
+ static #danglingLabelFindings(labels, nets) {
73
+ return labels
74
+ .filter(
75
+ (label) =>
76
+ !nets.some((net) =>
77
+ (net.labels || []).some((netLabel) =>
78
+ SchematicConnectivityQaBuilder.#sameTextPoint(
79
+ label,
80
+ netLabel
81
+ )
82
+ )
83
+ )
84
+ )
85
+ .map((label) => ({
86
+ code: 'schematic.connectivity.dangling-label',
87
+ severity: 'warning',
88
+ text: label.text,
89
+ x: label.x,
90
+ y: label.y
91
+ }))
92
+ }
93
+
94
+ /**
95
+ * Reports ports not assigned to any net.
96
+ * @param {object[]} ports Ports.
97
+ * @param {object[]} nets Normalized nets.
98
+ * @returns {object[]}
99
+ */
100
+ static #orphanPortFindings(ports, nets) {
101
+ return ports
102
+ .filter(
103
+ (port) =>
104
+ !nets.some((net) =>
105
+ (net.ports || []).some((netPort) =>
106
+ SchematicConnectivityQaBuilder.#sameNamedPoint(
107
+ port,
108
+ netPort
109
+ )
110
+ )
111
+ )
112
+ )
113
+ .map((port) => ({
114
+ code: 'schematic.connectivity.orphan-port',
115
+ severity: 'warning',
116
+ name: port.name,
117
+ x: port.x,
118
+ y: port.y
119
+ }))
120
+ }
121
+
122
+ /**
123
+ * Reports pins not assigned to any net.
124
+ * @param {object[]} pins Pins.
125
+ * @param {object[]} nets Normalized nets.
126
+ * @returns {object[]}
127
+ */
128
+ static #unconnectedPinFindings(pins, nets) {
129
+ return pins
130
+ .filter(
131
+ (pin) =>
132
+ !nets.some((net) =>
133
+ (net.pins || []).some((netPin) =>
134
+ SchematicConnectivityQaBuilder.#samePin(pin, netPin)
135
+ )
136
+ )
137
+ )
138
+ .map((pin) => ({
139
+ code: 'schematic.connectivity.unconnected-pin',
140
+ severity: 'warning',
141
+ ownerIndex: pin.ownerIndex,
142
+ designator: pin.designator,
143
+ name: pin.name,
144
+ x: pin.x,
145
+ y: pin.y
146
+ }))
147
+ }
148
+
149
+ /**
150
+ * Reports authored junctions that do not participate in any net.
151
+ * @param {object[]} junctions Authored junctions.
152
+ * @param {object[]} nets Normalized nets.
153
+ * @returns {object[]}
154
+ */
155
+ static #ambiguousJunctionFindings(junctions, nets) {
156
+ return junctions
157
+ .filter(
158
+ (junction) =>
159
+ !nets.some((net) =>
160
+ (net.junctions || []).some((netJunction) =>
161
+ SchematicConnectivityQaBuilder.#samePoint(
162
+ junction,
163
+ netJunction
164
+ )
165
+ )
166
+ )
167
+ )
168
+ .map((junction) => ({
169
+ code: 'schematic.connectivity.ambiguous-junction',
170
+ severity: 'warning',
171
+ x: junction.x,
172
+ y: junction.y
173
+ }))
174
+ }
175
+
176
+ /**
177
+ * Builds finding counters.
178
+ * @param {object[]} nets Normalized nets.
179
+ * @param {object[]} findings QA findings.
180
+ * @returns {object}
181
+ */
182
+ static #summary(nets, findings) {
183
+ return {
184
+ netCount: nets.length,
185
+ findingCount: findings.length,
186
+ danglingLabelCount: SchematicConnectivityQaBuilder.#countCode(
187
+ findings,
188
+ 'schematic.connectivity.dangling-label'
189
+ ),
190
+ orphanPortCount: SchematicConnectivityQaBuilder.#countCode(
191
+ findings,
192
+ 'schematic.connectivity.orphan-port'
193
+ ),
194
+ unconnectedPinCount: SchematicConnectivityQaBuilder.#countCode(
195
+ findings,
196
+ 'schematic.connectivity.unconnected-pin'
197
+ ),
198
+ implicitNetCount: SchematicConnectivityQaBuilder.#countCode(
199
+ findings,
200
+ 'schematic.connectivity.implicit-net-name'
201
+ ),
202
+ ambiguousJunctionCount: SchematicConnectivityQaBuilder.#countCode(
203
+ findings,
204
+ 'schematic.connectivity.ambiguous-junction'
205
+ )
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Counts findings with one code.
211
+ * @param {object[]} findings QA findings.
212
+ * @param {string} code Diagnostic code.
213
+ * @returns {number}
214
+ */
215
+ static #countCode(findings, code) {
216
+ return findings.filter((finding) => finding.code === code).length
217
+ }
218
+
219
+ /**
220
+ * Compares text rows by text and point.
221
+ * @param {object} left First row.
222
+ * @param {object} right Second row.
223
+ * @returns {boolean}
224
+ */
225
+ static #sameTextPoint(left, right) {
226
+ return (
227
+ String(left.text || '') === String(right.text || '') &&
228
+ SchematicConnectivityQaBuilder.#samePoint(left, right)
229
+ )
230
+ }
231
+
232
+ /**
233
+ * Compares named rows by name and point.
234
+ * @param {object} left First row.
235
+ * @param {object} right Second row.
236
+ * @returns {boolean}
237
+ */
238
+ static #sameNamedPoint(left, right) {
239
+ return (
240
+ String(left.name || '') === String(right.name || '') &&
241
+ SchematicConnectivityQaBuilder.#samePoint(left, right)
242
+ )
243
+ }
244
+
245
+ /**
246
+ * Compares pin rows by identity and point.
247
+ * @param {object} left First pin.
248
+ * @param {object} right Second pin.
249
+ * @returns {boolean}
250
+ */
251
+ static #samePin(left, right) {
252
+ return (
253
+ String(left.ownerIndex || '') === String(right.ownerIndex || '') &&
254
+ String(left.designator || '') === String(right.designator || '') &&
255
+ SchematicConnectivityQaBuilder.#samePoint(left, right)
256
+ )
257
+ }
258
+
259
+ /**
260
+ * Compares points with a small parser-tolerance.
261
+ * @param {object} left First point.
262
+ * @param {object} right Second point.
263
+ * @returns {boolean}
264
+ */
265
+ static #samePoint(left, right) {
266
+ return (
267
+ Math.abs(Number(left.x) - Number(right.x)) <= 0.01 &&
268
+ Math.abs(Number(left.y) - Number(right.y)) <= 0.01
269
+ )
270
+ }
271
+ }
@@ -0,0 +1,140 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ const { getDisplayText, getField, parseBoolean, parseNumericField, toColor } =
8
+ ParserUtils
9
+
10
+ /**
11
+ * Parses cross-sheet connector records into a first-class schematic sidecar.
12
+ */
13
+ export class SchematicCrossSheetConnectorParser {
14
+ static SCHEMA_ID = 'altium-toolkit.schematic.cross-sheet-connectors.a1'
15
+
16
+ /**
17
+ * Parses cross-sheet connector records.
18
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
19
+ * @returns {object | null}
20
+ */
21
+ static parse(records) {
22
+ const connectors = (records || [])
23
+ .filter((record) =>
24
+ SchematicCrossSheetConnectorParser.#isCrossSheetConnector(
25
+ record
26
+ )
27
+ )
28
+ .map((record) =>
29
+ SchematicCrossSheetConnectorParser.#connector(record)
30
+ )
31
+ .filter(Boolean)
32
+
33
+ if (!connectors.length) {
34
+ return null
35
+ }
36
+
37
+ return {
38
+ schema: SchematicCrossSheetConnectorParser.SCHEMA_ID,
39
+ connectors
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Returns true when a record is tagged as a cross-sheet connector.
45
+ * @param {object} record Schematic record.
46
+ * @returns {boolean}
47
+ */
48
+ static #isCrossSheetConnector(record) {
49
+ if (getField(record?.fields, 'RECORD') !== '17') {
50
+ return false
51
+ }
52
+
53
+ return (
54
+ parseBoolean(record.fields.IsCrossSheetConnector) ||
55
+ parseBoolean(record.fields.CrossSheetConnector) ||
56
+ parseBoolean(record.fields.ISCROSSSHEETCONNECTOR) ||
57
+ parseBoolean(record.fields.IsOffSheetConnector)
58
+ )
59
+ }
60
+
61
+ /**
62
+ * Parses one cross-sheet connector record.
63
+ * @param {object} record Schematic record.
64
+ * @returns {object | null}
65
+ */
66
+ static #connector(record) {
67
+ const x = parseNumericField(record.fields, 'Location.X')
68
+ const y = parseNumericField(record.fields, 'Location.Y')
69
+
70
+ if (x === null || y === null) {
71
+ return null
72
+ }
73
+
74
+ const indexInSheet =
75
+ parseNumericField(record.fields, 'IndexInSheet') ??
76
+ record.recordIndex ??
77
+ 0
78
+ const name =
79
+ getDisplayText(record.fields) || getField(record.fields, 'Name')
80
+
81
+ return SchematicCrossSheetConnectorParser.#stripEmpty({
82
+ key: 'cross-sheet-connector-' + String(indexInSheet),
83
+ recordKey: SchematicCrossSheetConnectorParser.#recordKey(record),
84
+ recordId: 'record-' + String(indexInSheet),
85
+ name,
86
+ x,
87
+ y,
88
+ style: SchematicCrossSheetConnectorParser.#style(
89
+ parseNumericField(record.fields, 'Style') ??
90
+ parseNumericField(record.fields, 'Orientation') ??
91
+ 0
92
+ ),
93
+ color: toColor(record.fields.Color, '#000000')
94
+ })
95
+ }
96
+
97
+ /**
98
+ * Resolves a connector style code.
99
+ * @param {number} value Style code.
100
+ * @returns {'left' | 'right' | 'top' | 'bottom'}
101
+ */
102
+ static #style(value) {
103
+ switch (Number(value)) {
104
+ case 1:
105
+ return 'right'
106
+ case 2:
107
+ return 'top'
108
+ case 3:
109
+ return 'bottom'
110
+ case 0:
111
+ default:
112
+ return 'left'
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Builds a stable schematic record key.
118
+ * @param {object} record Schematic record.
119
+ * @returns {string}
120
+ */
121
+ static #recordKey(record) {
122
+ return 'schematic-record-' + String(record?.recordIndex ?? 0)
123
+ }
124
+
125
+ /**
126
+ * Drops empty optional fields.
127
+ * @param {Record<string, unknown>} value Source object.
128
+ * @returns {Record<string, unknown>}
129
+ */
130
+ static #stripEmpty(value) {
131
+ return Object.fromEntries(
132
+ Object.entries(value).filter(([, entry]) => {
133
+ if (Array.isArray(entry)) {
134
+ return entry.length > 0
135
+ }
136
+ return entry !== null && entry !== undefined && entry !== ''
137
+ })
138
+ )
139
+ }
140
+ }