altium-toolkit 1.1.3 → 1.1.23

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 (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +7 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,380 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds summaries of preserved parser data that is not modeled yet.
7
+ */
8
+ export class UnsupportedFeatureReportBuilder {
9
+ static SCHEMA = 'altium-toolkit.unsupported-features.a1'
10
+
11
+ /**
12
+ * Builds an unsupported feature report.
13
+ * @param {{ models?: object[], recordTypes?: object[], rawRecords?: object[], opaqueRecords?: object[], diagnostics?: object[] }} [input] Report input.
14
+ * @returns {object}
15
+ */
16
+ static build(input = {}) {
17
+ const models = UnsupportedFeatureReportBuilder.#models(input)
18
+ const recordTypes =
19
+ UnsupportedFeatureReportBuilder.#unsupportedRecordTypes(
20
+ input,
21
+ models
22
+ )
23
+ const rawRecords = UnsupportedFeatureReportBuilder.#rawRecords(
24
+ input,
25
+ models
26
+ )
27
+ const opaqueRecords = UnsupportedFeatureReportBuilder.#opaqueRecords(
28
+ input,
29
+ models
30
+ )
31
+ const diagnostics = UnsupportedFeatureReportBuilder.#diagnostics(
32
+ input,
33
+ models
34
+ )
35
+ const itemCount =
36
+ recordTypes.length +
37
+ rawRecords.length +
38
+ opaqueRecords.length +
39
+ diagnostics.length
40
+
41
+ return {
42
+ schema: UnsupportedFeatureReportBuilder.SCHEMA,
43
+ summary: {
44
+ modelCount: models.length,
45
+ unsupportedRecordTypeCount: recordTypes.length,
46
+ rawRecordCount: rawRecords.length,
47
+ opaqueRecordCount: opaqueRecords.length,
48
+ diagnosticCount: diagnostics.length,
49
+ itemCount,
50
+ status: itemCount ? 'unsupported' : 'supported'
51
+ },
52
+ recordTypes,
53
+ rawRecords,
54
+ opaqueRecords,
55
+ diagnostics
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Normalizes model input.
61
+ * @param {object} input Report input.
62
+ * @returns {object[]}
63
+ */
64
+ static #models(input) {
65
+ return Array.isArray(input?.models) ? input.models : []
66
+ }
67
+
68
+ /**
69
+ * Collects unsupported record type summaries.
70
+ * @param {object} input Report input.
71
+ * @param {object[]} models Parser roots.
72
+ * @returns {object[]}
73
+ */
74
+ static #unsupportedRecordTypes(input, models) {
75
+ return [
76
+ ...(Array.isArray(input?.recordTypes)
77
+ ? input.recordTypes.map((recordType) => ({
78
+ row: recordType || {},
79
+ fileName: recordType?.fileName || '',
80
+ domain: recordType?.domain || ''
81
+ }))
82
+ : []),
83
+ ...models.flatMap((model) =>
84
+ UnsupportedFeatureReportBuilder.#modelRecordTypes(model)
85
+ )
86
+ ]
87
+ .filter(({ row }) => row?.supported === false)
88
+ .map(({ row, fileName, domain }) =>
89
+ UnsupportedFeatureReportBuilder.#stripUndefined({
90
+ fileName,
91
+ domain,
92
+ recordType:
93
+ UnsupportedFeatureReportBuilder.#finiteOrUndefined(
94
+ row.recordType
95
+ ),
96
+ name: row.name,
97
+ family: row.family,
98
+ count: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
99
+ row.count
100
+ )
101
+ })
102
+ )
103
+ }
104
+
105
+ /**
106
+ * Collects record type rows from one parser root.
107
+ * @param {object} model Parser root.
108
+ * @returns {{ row: object, fileName: string, domain: string }[]}
109
+ */
110
+ static #modelRecordTypes(model) {
111
+ const fileName = String(model?.fileName || '')
112
+ return [
113
+ ...UnsupportedFeatureReportBuilder.#domainRows(
114
+ model?.schematic?.recordTypes,
115
+ fileName,
116
+ 'schematic'
117
+ ),
118
+ ...UnsupportedFeatureReportBuilder.#domainRows(
119
+ model?.schematicLibrary?.recordTypes,
120
+ fileName,
121
+ 'schematic-library'
122
+ )
123
+ ]
124
+ }
125
+
126
+ /**
127
+ * Wraps domain rows with file and domain metadata.
128
+ * @param {object[] | undefined} rows Rows.
129
+ * @param {string} fileName Source file name.
130
+ * @param {string} domain Parser domain.
131
+ * @returns {{ row: object, fileName: string, domain: string }[]}
132
+ */
133
+ static #domainRows(rows, fileName, domain) {
134
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
135
+ row,
136
+ fileName,
137
+ domain
138
+ }))
139
+ }
140
+
141
+ /**
142
+ * Collects unsupported or unparsed raw record rows.
143
+ * @param {object} input Report input.
144
+ * @param {object[]} models Parser roots.
145
+ * @returns {object[]}
146
+ */
147
+ static #rawRecords(input, models) {
148
+ return [
149
+ ...(Array.isArray(input?.rawRecords)
150
+ ? input.rawRecords.map((record) => ({
151
+ record: record || {},
152
+ fileName: record?.fileName || '',
153
+ domain: record?.domain || 'pcb'
154
+ }))
155
+ : []),
156
+ ...models.flatMap((model) =>
157
+ UnsupportedFeatureReportBuilder.#modelRawRecords(model)
158
+ )
159
+ ]
160
+ .filter(
161
+ ({ record }) =>
162
+ record?.supported === false || record?.parsed === false
163
+ )
164
+ .map(({ record, fileName, domain }) =>
165
+ UnsupportedFeatureReportBuilder.#stripUndefined({
166
+ fileName,
167
+ domain,
168
+ sourceStream: record.sourceStream,
169
+ sourceStorage: record.sourceStorage,
170
+ recordIndex:
171
+ UnsupportedFeatureReportBuilder.#finiteOrUndefined(
172
+ record.recordIndex
173
+ ),
174
+ family: record.family,
175
+ type: record.type,
176
+ typeId: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
177
+ record.typeId
178
+ ),
179
+ byteLength:
180
+ UnsupportedFeatureReportBuilder.#finiteOrUndefined(
181
+ record.byteLength
182
+ ),
183
+ supported: record.supported,
184
+ parsed: record.parsed
185
+ })
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Collects raw records from one parser root.
191
+ * @param {object} model Parser root.
192
+ * @returns {{ record: object, fileName: string, domain: string }[]}
193
+ */
194
+ static #modelRawRecords(model) {
195
+ const fileName = String(model?.fileName || '')
196
+ return [
197
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
198
+ model?.pcb?.rawRecords,
199
+ fileName,
200
+ 'pcb'
201
+ ),
202
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
203
+ (model?.pcbLibrary?.footprints || []).flatMap(
204
+ (footprint) => footprint.rawRecords || []
205
+ ),
206
+ fileName,
207
+ 'pcb-library'
208
+ ),
209
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
210
+ model?.schematic?.rawRecords,
211
+ fileName,
212
+ 'schematic'
213
+ ),
214
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
215
+ (model?.schematicLibrary?.components || []).flatMap(
216
+ (component) => component.rawRecords || []
217
+ ),
218
+ fileName,
219
+ 'schematic-library'
220
+ )
221
+ ]
222
+ }
223
+
224
+ /**
225
+ * Wraps raw record rows with source metadata.
226
+ * @param {object[] | undefined} records Raw records.
227
+ * @param {string} fileName Source file name.
228
+ * @param {string} domain Parser domain.
229
+ * @returns {{ record: object, fileName: string, domain: string }[]}
230
+ */
231
+ static #rawDomainRows(records, fileName, domain) {
232
+ return (Array.isArray(records) ? records : []).map((record) => ({
233
+ record,
234
+ fileName,
235
+ domain
236
+ }))
237
+ }
238
+
239
+ /**
240
+ * Collects opaque preserved rows.
241
+ * @param {object} input Report input.
242
+ * @param {object[]} models Parser roots.
243
+ * @returns {object[]}
244
+ */
245
+ static #opaqueRecords(input, models) {
246
+ return [
247
+ ...(Array.isArray(input?.opaqueRecords)
248
+ ? input.opaqueRecords.map((record) => ({
249
+ record: record || {},
250
+ fileName: record?.fileName || '',
251
+ domain: record?.domain || 'schematic'
252
+ }))
253
+ : []),
254
+ ...models.flatMap((model) =>
255
+ UnsupportedFeatureReportBuilder.#modelOpaqueRecords(model)
256
+ )
257
+ ].map(({ record, fileName, domain }) =>
258
+ UnsupportedFeatureReportBuilder.#stripUndefined({
259
+ fileName,
260
+ domain,
261
+ sourceStream: record.sourceStream,
262
+ sourceStorage: record.sourceStorage,
263
+ frameType: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
264
+ record.frameType
265
+ ),
266
+ recordIndex: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
267
+ record.recordIndex
268
+ ),
269
+ byteLength: UnsupportedFeatureReportBuilder.#finiteOrUndefined(
270
+ record.byteLength
271
+ )
272
+ })
273
+ )
274
+ }
275
+
276
+ /**
277
+ * Collects opaque rows from one parser root.
278
+ * @param {object} model Parser root.
279
+ * @returns {{ record: object, fileName: string, domain: string }[]}
280
+ */
281
+ static #modelOpaqueRecords(model) {
282
+ const fileName = String(model?.fileName || '')
283
+ return [
284
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
285
+ model?.schematic?.opaqueRecords,
286
+ fileName,
287
+ 'schematic'
288
+ ),
289
+ ...UnsupportedFeatureReportBuilder.#rawDomainRows(
290
+ (model?.schematicLibrary?.components || []).flatMap(
291
+ (component) => component.opaqueRecords || []
292
+ ),
293
+ fileName,
294
+ 'schematic-library'
295
+ )
296
+ ].map(({ record, ...metadata }) => ({ record, ...metadata }))
297
+ }
298
+
299
+ /**
300
+ * Collects unsupported diagnostics.
301
+ * @param {object} input Report input.
302
+ * @param {object[]} models Parser roots.
303
+ * @returns {object[]}
304
+ */
305
+ static #diagnostics(input, models) {
306
+ return [
307
+ ...(Array.isArray(input?.diagnostics)
308
+ ? input.diagnostics.map((diagnostic) => ({
309
+ diagnostic: diagnostic || {},
310
+ fileName: diagnostic?.fileName || ''
311
+ }))
312
+ : []),
313
+ ...models.flatMap((model) =>
314
+ (model?.diagnostics || []).map((diagnostic) => ({
315
+ diagnostic,
316
+ fileName: model.fileName || diagnostic.fileName || ''
317
+ }))
318
+ )
319
+ ]
320
+ .filter(({ diagnostic }) =>
321
+ UnsupportedFeatureReportBuilder.#isUnsupportedDiagnostic(
322
+ diagnostic
323
+ )
324
+ )
325
+ .map(({ diagnostic, fileName }) =>
326
+ UnsupportedFeatureReportBuilder.#stripUndefined({
327
+ fileName,
328
+ code: diagnostic.code,
329
+ severity: diagnostic.severity,
330
+ message: diagnostic.message,
331
+ source: diagnostic.source,
332
+ sourceStream: diagnostic.sourceStream,
333
+ sourceStorage: diagnostic.sourceStorage,
334
+ recordIndex:
335
+ UnsupportedFeatureReportBuilder.#finiteOrUndefined(
336
+ diagnostic.recordIndex
337
+ ),
338
+ recordType:
339
+ UnsupportedFeatureReportBuilder.#finiteOrUndefined(
340
+ diagnostic.recordType
341
+ ),
342
+ errorKind: diagnostic.errorKind
343
+ })
344
+ )
345
+ }
346
+
347
+ /**
348
+ * Returns true for unsupported-feature diagnostics.
349
+ * @param {object} diagnostic Diagnostic row.
350
+ * @returns {boolean}
351
+ */
352
+ static #isUnsupportedDiagnostic(diagnostic) {
353
+ if (!diagnostic || typeof diagnostic !== 'object') return false
354
+ if (diagnostic.errorKind === 'unsupported-feature') return true
355
+ return String(diagnostic.code || '')
356
+ .toLowerCase()
357
+ .includes('unsupported')
358
+ }
359
+
360
+ /**
361
+ * Returns a finite number or undefined.
362
+ * @param {unknown} value Candidate value.
363
+ * @returns {number | undefined}
364
+ */
365
+ static #finiteOrUndefined(value) {
366
+ const number = Number(value)
367
+ return Number.isFinite(number) ? number : undefined
368
+ }
369
+
370
+ /**
371
+ * Removes undefined values from one row.
372
+ * @param {object} row Row to clean.
373
+ * @returns {object}
374
+ */
375
+ static #stripUndefined(row) {
376
+ return Object.fromEntries(
377
+ Object.entries(row).filter(([, value]) => value !== undefined)
378
+ )
379
+ }
380
+ }