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,302 @@
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, parseNumericField, toColor } = ParserUtils
8
+
9
+ /**
10
+ * Parses schematic harness connector records into a first-class read model.
11
+ */
12
+ export class SchematicHarnessParser {
13
+ static SCHEMA_ID = 'altium-toolkit.schematic.harness.a1'
14
+
15
+ /**
16
+ * Parses harness connectors, entries, type labels, and signal harnesses.
17
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
18
+ * @returns {object | null}
19
+ */
20
+ static parse(records) {
21
+ const connectors = (records || [])
22
+ .filter((record) => getField(record.fields, 'RECORD') === '215')
23
+ .map((record) => SchematicHarnessParser.#connector(record, records))
24
+ const signalHarnesses = (records || [])
25
+ .filter((record) => getField(record.fields, 'RECORD') === '218')
26
+ .map((record) => SchematicHarnessParser.#signalHarness(record))
27
+ .filter(Boolean)
28
+
29
+ if (!connectors.length && !signalHarnesses.length) {
30
+ return null
31
+ }
32
+
33
+ return {
34
+ schema: SchematicHarnessParser.SCHEMA_ID,
35
+ connectors,
36
+ signalHarnesses,
37
+ bundleLinks: SchematicHarnessParser.#bundleLinks(
38
+ connectors,
39
+ signalHarnesses
40
+ )
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Parses one harness connector and its owned children.
46
+ * @param {object} record Connector record.
47
+ * @param {object[]} records All records.
48
+ * @returns {object}
49
+ */
50
+ static #connector(record, records) {
51
+ const ownerKeys = new Set(SchematicHarnessParser.#ownerKeys(record))
52
+ const entries = (records || [])
53
+ .filter(
54
+ (candidate) =>
55
+ getField(candidate.fields, 'RECORD') === '216' &&
56
+ ownerKeys.has(getField(candidate.fields, 'OwnerIndex'))
57
+ )
58
+ .map((entry) => SchematicHarnessParser.#entry(entry))
59
+ const typeLabelRecord = (records || []).find(
60
+ (candidate) =>
61
+ getField(candidate.fields, 'RECORD') === '217' &&
62
+ ownerKeys.has(getField(candidate.fields, 'OwnerIndex'))
63
+ )
64
+ const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
65
+ const connectorKey =
66
+ 'harness-connector-' + String(indexInSheet ?? record.recordIndex)
67
+
68
+ return SchematicHarnessParser.#stripEmpty({
69
+ key: connectorKey,
70
+ recordKey: SchematicHarnessParser.#recordKey(record),
71
+ recordId: SchematicHarnessParser.#recordId(record),
72
+ x: parseNumericField(record.fields, 'Location.X') || 0,
73
+ y: parseNumericField(record.fields, 'Location.Y') || 0,
74
+ width: parseNumericField(record.fields, 'XSize') || 0,
75
+ height: parseNumericField(record.fields, 'YSize') || 0,
76
+ side: SchematicHarnessParser.#side(
77
+ parseNumericField(record.fields, 'Side')
78
+ ),
79
+ primaryConnectionPosition:
80
+ parseNumericField(record.fields, 'PrimaryConnectionPosition') ||
81
+ 0,
82
+ lineWidth: parseNumericField(record.fields, 'LineWidth') || 0,
83
+ color: toColor(record.fields.Color, '#000000'),
84
+ fill: toColor(record.fields.AreaColor, '#ffffff'),
85
+ entries,
86
+ typeLabel: typeLabelRecord
87
+ ? SchematicHarnessParser.#typeLabel(typeLabelRecord)
88
+ : null
89
+ })
90
+ }
91
+
92
+ /**
93
+ * Parses one harness entry.
94
+ * @param {object} record Entry record.
95
+ * @returns {object}
96
+ */
97
+ static #entry(record) {
98
+ return SchematicHarnessParser.#stripEmpty({
99
+ key: 'harness-entry-' + String(record.recordIndex ?? 0),
100
+ recordKey: SchematicHarnessParser.#recordKey(record),
101
+ name: getField(record.fields, 'Name'),
102
+ side: SchematicHarnessParser.#side(
103
+ parseNumericField(record.fields, 'Side')
104
+ ),
105
+ distanceFromTop: SchematicHarnessParser.#distanceFromTop(
106
+ record.fields
107
+ ),
108
+ harnessType: getField(record.fields, 'HarnessType'),
109
+ textStyle: SchematicHarnessParser.#textStyle(
110
+ getField(record.fields, 'TextStyle')
111
+ ),
112
+ textColor: toColor(record.fields.TextColor, '#000000')
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Parses one harness type label.
118
+ * @param {object} record Type-label record.
119
+ * @returns {object}
120
+ */
121
+ static #typeLabel(record) {
122
+ return SchematicHarnessParser.#stripEmpty({
123
+ key: 'harness-type-' + String(record.recordIndex ?? 0),
124
+ recordKey: SchematicHarnessParser.#recordKey(record),
125
+ text: getDisplayText(record.fields),
126
+ x: parseNumericField(record.fields, 'Location.X') || 0,
127
+ y: parseNumericField(record.fields, 'Location.Y') || 0,
128
+ color: toColor(record.fields.Color, '#000000')
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Parses one signal harness polyline.
134
+ * @param {object} record Signal-harness record.
135
+ * @returns {object | null}
136
+ */
137
+ static #signalHarness(record) {
138
+ const points = SchematicHarnessParser.#points(record.fields)
139
+ if (points.length < 2) {
140
+ return null
141
+ }
142
+
143
+ return SchematicHarnessParser.#stripEmpty({
144
+ key: 'signal-harness-' + String(record.recordIndex ?? 0),
145
+ recordKey: SchematicHarnessParser.#recordKey(record),
146
+ points,
147
+ color: toColor(record.fields.Color, '#9fc5e8'),
148
+ lineWidth: parseNumericField(record.fields, 'LineWidth') || 1
149
+ })
150
+ }
151
+
152
+ /**
153
+ * Builds high-level bundle links between connectors and signal harnesses.
154
+ * @param {object[]} connectors Connector rows.
155
+ * @param {object[]} signalHarnesses Signal harness rows.
156
+ * @returns {object[]}
157
+ */
158
+ static #bundleLinks(connectors, signalHarnesses) {
159
+ return (connectors || []).map((connector, index) =>
160
+ SchematicHarnessParser.#stripEmpty({
161
+ key: 'harness-bundle-' + index,
162
+ connectorKey: connector.key,
163
+ harnessType:
164
+ connector.typeLabel?.text ||
165
+ connector.entries?.find((entry) => entry.harnessType)
166
+ ?.harnessType,
167
+ entries: (connector.entries || []).map((entry) => entry.name),
168
+ signalHarnessKeys: (signalHarnesses || []).map(
169
+ (signalHarness) => signalHarness.key
170
+ )
171
+ })
172
+ )
173
+ }
174
+
175
+ /**
176
+ * Converts harness-entry distance fields into mils.
177
+ * @param {Record<string, string | string[]>} fields Record fields.
178
+ * @returns {number}
179
+ */
180
+ static #distanceFromTop(fields) {
181
+ const whole = parseNumericField(fields, 'DistanceFromTop') || 0
182
+ const fraction = parseNumericField(fields, 'DistanceFromTop_Frac1') || 0
183
+ return Number(((whole + fraction / 1000000) * 100).toFixed(4))
184
+ }
185
+
186
+ /**
187
+ * Parses point-list fields.
188
+ * @param {Record<string, string | string[]>} fields Record fields.
189
+ * @returns {{ x: number, y: number }[]}
190
+ */
191
+ static #points(fields) {
192
+ const count = parseNumericField(fields, 'LocationCount') || 0
193
+ const points = []
194
+
195
+ for (let index = 1; index <= count; index += 1) {
196
+ const x = parseNumericField(fields, 'X' + index)
197
+ const y = parseNumericField(fields, 'Y' + index)
198
+ if (x === null || y === null) {
199
+ continue
200
+ }
201
+ points.push({ x, y })
202
+ }
203
+
204
+ return points
205
+ }
206
+
207
+ /**
208
+ * Resolves side codes.
209
+ * @param {number | null} value Side code.
210
+ * @returns {'left' | 'right' | 'top' | 'bottom'}
211
+ */
212
+ static #side(value) {
213
+ switch (value) {
214
+ case 1:
215
+ return 'right'
216
+ case 2:
217
+ return 'top'
218
+ case 3:
219
+ return 'bottom'
220
+ case 0:
221
+ default:
222
+ return 'left'
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Resolves text-style labels.
228
+ * @param {string} value Raw text-style value.
229
+ * @returns {string}
230
+ */
231
+ static #textStyle(value) {
232
+ const normalized = String(value || '').toLowerCase()
233
+ if (normalized === '1' || normalized === 'abbreviated') {
234
+ return 'abbreviated'
235
+ }
236
+ if (normalized === '2' || normalized === 'short') {
237
+ return 'short'
238
+ }
239
+ return 'full'
240
+ }
241
+
242
+ /**
243
+ * Builds owner lookup keys for a connector record.
244
+ * @param {object} record Connector record.
245
+ * @returns {string[]}
246
+ */
247
+ static #ownerKeys(record) {
248
+ const recordIndex = Number(record?.recordIndex)
249
+ const indexInSheet = parseNumericField(record?.fields, 'IndexInSheet')
250
+ const keys = new Set()
251
+
252
+ if (Number.isInteger(recordIndex)) {
253
+ keys.add(String(recordIndex))
254
+ keys.add(String(recordIndex + 1))
255
+ }
256
+ if (Number.isInteger(indexInSheet)) {
257
+ keys.add(String(indexInSheet))
258
+ keys.add(String(indexInSheet + 1))
259
+ }
260
+
261
+ return [...keys]
262
+ }
263
+
264
+ /**
265
+ * Builds a stable record id.
266
+ * @param {object} record Source record.
267
+ * @returns {string}
268
+ */
269
+ static #recordId(record) {
270
+ const indexInSheet = parseNumericField(record?.fields, 'IndexInSheet')
271
+ return 'record-' + String(indexInSheet ?? record?.recordIndex ?? 0)
272
+ }
273
+
274
+ /**
275
+ * Builds a stable record key.
276
+ * @param {object} record Source record.
277
+ * @returns {string}
278
+ */
279
+ static #recordKey(record) {
280
+ return 'schematic-record-' + String(record?.recordIndex ?? 0)
281
+ }
282
+
283
+ /**
284
+ * Removes empty fields while preserving false and zero.
285
+ * @param {Record<string, unknown>} value Candidate object.
286
+ * @returns {Record<string, unknown>}
287
+ */
288
+ static #stripEmpty(value) {
289
+ return Object.fromEntries(
290
+ Object.entries(value || {}).filter(([, entryValue]) => {
291
+ if (Array.isArray(entryValue)) {
292
+ return entryValue.length > 0
293
+ }
294
+ return (
295
+ entryValue !== null &&
296
+ entryValue !== undefined &&
297
+ entryValue !== ''
298
+ )
299
+ })
300
+ )
301
+ }
302
+ }