altium-toolkit 1.1.22 → 1.1.24

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 (99) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +134 -23
  3. package/docs/model-format.md +174 -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 +511 -1
  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 +2 -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/src/core/BinaryReader.mjs +213 -2
  35. package/src/core/altium/AltiumParser.mjs +352 -14
  36. package/src/core/altium/AltiumUnits.mjs +205 -0
  37. package/src/core/altium/AsciiRecordParser.mjs +9 -0
  38. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  39. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  40. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  41. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  42. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  43. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  44. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  45. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  46. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  47. package/src/core/altium/ParameterCollection.mjs +431 -0
  48. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  49. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  50. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  51. package/src/core/altium/ParserErrors.mjs +90 -0
  52. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  53. package/src/core/altium/ParserUtils.mjs +24 -0
  54. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  55. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  56. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  57. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  58. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  59. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  60. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  61. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  62. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  63. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  64. package/src/core/altium/PcbModelParser.mjs +223 -4
  65. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  66. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  67. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  68. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  69. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  70. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  71. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  72. package/src/core/altium/SchLibModelParser.mjs +840 -0
  73. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  74. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  75. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  76. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  77. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  78. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  79. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  80. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  81. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  82. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  83. package/src/core/altium/SchematicPinParser.mjs +12 -45
  84. package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
  85. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  86. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  87. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  88. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  89. package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
  90. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  91. package/src/core/altium/SchematicTextParser.mjs +28 -12
  92. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  93. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  94. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  95. package/src/parser.mjs +35 -1
  96. package/src/renderers.mjs +1 -0
  97. package/src/ui/SchematicShapeRenderer.mjs +49 -6
  98. package/src/ui/SchematicSvgRenderer.mjs +37 -8
  99. package/src/ui/SchematicTypography.mjs +4 -3
@@ -13,12 +13,16 @@ import { SchematicPinParser } from './SchematicPinParser.mjs'
13
13
  import { SchematicPrimitiveParser } from './SchematicPrimitiveParser.mjs'
14
14
  import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
15
15
  import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
16
+ import { ParserDiagnosticNormalizer } from './ParserDiagnosticNormalizer.mjs'
17
+ import { AltiumUnsupportedFeatureError } from './ParserErrors.mjs'
16
18
  import { IntLibModelParser } from './IntLibModelParser.mjs'
17
19
  import { IntLibStreamExtractor } from './IntLibStreamExtractor.mjs'
18
20
  import { DraftsmanDigestParser } from './DraftsmanDigestParser.mjs'
19
21
  import { PcbModelParser } from './PcbModelParser.mjs'
20
22
  import { PcbLibModelParser } from './PcbLibModelParser.mjs'
21
23
  import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
24
+ import { SchLibModelParser } from './SchLibModelParser.mjs'
25
+ import { SchLibStreamExtractor } from './SchLibStreamExtractor.mjs'
22
26
  import { PrjPcbModelParser } from './PrjPcbModelParser.mjs'
23
27
  import { PrjScrModelParser } from './PrjScrModelParser.mjs'
24
28
  import { PcbStreamExtractor } from './PcbStreamExtractor.mjs'
@@ -35,8 +39,11 @@ import { SchematicComponentOwnerTextResolver } from './SchematicComponentOwnerTe
35
39
  import { SchematicOwnershipGraphParser } from './SchematicOwnershipGraphParser.mjs'
36
40
  import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
37
41
  import { SchematicTemplateParser } from './SchematicTemplateParser.mjs'
42
+ import { SchematicThumbnailParser } from './SchematicThumbnailParser.mjs'
38
43
  import { SchematicHarnessParser } from './SchematicHarnessParser.mjs'
39
44
  import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
45
+ import { SchematicCodeSymbolParser } from './SchematicCodeSymbolParser.mjs'
46
+ import { SchematicHyperlinkParser } from './SchematicHyperlinkParser.mjs'
40
47
  import { SchematicCrossSheetConnectorParser } from './SchematicCrossSheetConnectorParser.mjs'
41
48
  import { SchematicRepeatedChannelParser } from './SchematicRepeatedChannelParser.mjs'
42
49
  import { SchematicDisplayModeCatalogParser } from './SchematicDisplayModeCatalogParser.mjs'
@@ -51,6 +58,7 @@ const {
51
58
  getField,
52
59
  parseBoolean,
53
60
  parseNumericField,
61
+ parseSchematicLineWidth,
54
62
  toColor,
55
63
  dedupeByDesignator,
56
64
  stripExtension
@@ -88,11 +96,35 @@ export class AltiumParser {
88
96
  )
89
97
  }
90
98
 
99
+ /**
100
+ * Parses a native Altium buffer into a non-throwing Circuit JSON envelope.
101
+ * @param {string} fileName
102
+ * @param {ArrayBuffer} arrayBuffer
103
+ * @returns {{ ok: true, model: object[], diagnostics: object[] } | { ok: false, model: null, diagnostics: object[] }}
104
+ */
105
+ static tryParseArrayBuffer(fileName, arrayBuffer) {
106
+ return AltiumParser.#tryParse(fileName, () =>
107
+ AltiumParser.parseArrayBuffer(fileName, arrayBuffer)
108
+ )
109
+ }
110
+
111
+ /**
112
+ * Parses a native Altium buffer into a non-throwing renderer model envelope.
113
+ * @param {string} fileName
114
+ * @param {ArrayBuffer} arrayBuffer
115
+ * @returns {{ ok: true, model: object, diagnostics: object[] } | { ok: false, model: null, diagnostics: object[] }}
116
+ */
117
+ static tryParseArrayBufferToRendererModel(fileName, arrayBuffer) {
118
+ return AltiumParser.#tryParse(fileName, () =>
119
+ AltiumParser.parseArrayBufferToRendererModel(fileName, arrayBuffer)
120
+ )
121
+ }
122
+
91
123
  /**
92
124
  * Parses a native Altium buffer into the renderer compatibility model.
93
125
  * @param {string} fileName
94
126
  * @param {ArrayBuffer} arrayBuffer
95
- * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project' | 'project-script' | 'integrated-library' | 'draftsman', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'PrjScr' | 'IntLib' | 'PCBDwf', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, projectScript?: Record<string, unknown>, integratedLibrary?: Record<string, unknown>, draftsman?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
127
+ * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'schematic-library' | 'project' | 'project-script' | 'integrated-library' | 'draftsman', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'SchLib' | 'PrjPcb' | 'PrjScr' | 'IntLib' | 'PCBDwf', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, schematicLibrary?: Record<string, unknown>, project?: Record<string, unknown>, projectScript?: Record<string, unknown>, integratedLibrary?: Record<string, unknown>, draftsman?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
96
128
  */
97
129
  static parseArrayBufferToRendererModel(fileName, arrayBuffer) {
98
130
  const records = AsciiRecordParser.parse(arrayBuffer)
@@ -122,6 +154,12 @@ export class AltiumParser {
122
154
  PcbLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
123
155
  )
124
156
  }
157
+ if (fileType === 'SchLib') {
158
+ return SchLibModelParser.parse(
159
+ fileName,
160
+ SchLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
161
+ )
162
+ }
125
163
  if (fileType === 'PrjPcb') {
126
164
  return PrjPcbModelParser.parse(fileName, arrayBuffer)
127
165
  }
@@ -137,20 +175,78 @@ export class AltiumParser {
137
175
  if (fileType === 'PCBDwf') {
138
176
  return DraftsmanDigestParser.parse(fileName, arrayBuffer)
139
177
  }
140
- throw new Error('Unsupported file type: ' + fileName)
178
+ throw new AltiumUnsupportedFeatureError(
179
+ 'Unsupported file type: ' + fileName,
180
+ { fileName }
181
+ )
182
+ }
183
+
184
+ /**
185
+ * Runs one parser function and returns a normalized result envelope.
186
+ * @param {string} fileName
187
+ * @param {() => object | object[]} parser
188
+ * @returns {{ ok: true, model: object | object[], diagnostics: object[] } | { ok: false, model: null, diagnostics: object[] }}
189
+ */
190
+ static #tryParse(fileName, parser) {
191
+ try {
192
+ const model = parser()
193
+ return {
194
+ ok: true,
195
+ model,
196
+ diagnostics: ParserDiagnosticNormalizer.normalizeMany(
197
+ model?.diagnostics || [],
198
+ { source: fileName }
199
+ )
200
+ }
201
+ } catch (error) {
202
+ return AltiumParser.#safeParseFailure(fileName, error)
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Builds one safe-parse failure envelope.
208
+ * @param {string} fileName
209
+ * @param {unknown} error
210
+ * @returns {{ ok: false, model: null, diagnostics: object[] }}
211
+ */
212
+ static #safeParseFailure(fileName, error) {
213
+ const diagnostic = ParserDiagnosticNormalizer.normalize(error, {
214
+ code: 'parser.safe.parse.failed',
215
+ severity: 'error',
216
+ source: fileName
217
+ })
218
+ const errorKind = diagnostic.errorKind || 'parse'
219
+
220
+ return {
221
+ ok: false,
222
+ model: null,
223
+ diagnostics: [
224
+ {
225
+ ...diagnostic,
226
+ code: 'parser.safe.parse.failed',
227
+ fileName: String(fileName || ''),
228
+ errorKind,
229
+ errorName:
230
+ error && typeof error === 'object' && 'name' in error
231
+ ? String(error.name)
232
+ : 'Error'
233
+ }
234
+ ]
235
+ }
141
236
  }
142
237
 
143
238
  /**
144
239
  * Chooses the format based on extension and content.
145
240
  * @param {string} fileName
146
241
  * @param {{ fields: Record<string, string | string[]> }[]} records
147
- * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'PrjScr' | 'IntLib' | 'PCBDwf'}
242
+ * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'SchLib' | 'PrjPcb' | 'PrjScr' | 'IntLib' | 'PCBDwf'}
148
243
  */
149
244
  static #sniffFileType(fileName, records) {
150
245
  const normalized = String(fileName || '').toLowerCase()
151
246
  if (normalized.endsWith('.schdoc')) return 'SchDoc'
152
247
  if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
153
248
  if (normalized.endsWith('.pcblib')) return 'PcbLib'
249
+ if (normalized.endsWith('.schlib')) return 'SchLib'
154
250
  if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
155
251
  if (normalized.endsWith('.prjscr')) return 'PrjScr'
156
252
  if (normalized.endsWith('.intlib')) return 'IntLib'
@@ -159,7 +255,14 @@ export class AltiumParser {
159
255
  const hasSchematicHeader = records.some((record) =>
160
256
  getField(record.fields, 'HEADER').includes('Schematic')
161
257
  )
162
- return hasSchematicHeader ? 'SchDoc' : 'PcbDoc'
258
+ const hasSchematicLibraryHeader = records.some((record) =>
259
+ getField(record.fields, 'HEADER').includes('Schematic Library')
260
+ )
261
+ return hasSchematicLibraryHeader
262
+ ? 'SchLib'
263
+ : hasSchematicHeader
264
+ ? 'SchDoc'
265
+ : 'PcbDoc'
163
266
  }
164
267
  /**
165
268
  * Normalizes a schematic document.
@@ -185,6 +288,11 @@ export class AltiumParser {
185
288
  const recordTypes = SchematicRecordTypeRegistry.summarize(records)
186
289
  const ownersWithImplicitDisplayMode =
187
290
  AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
291
+ const activeOwnerDisplayModes =
292
+ AltiumParser.#collectActiveOwnerDisplayModes(
293
+ recordIndexAwareRecords,
294
+ componentRecords
295
+ )
188
296
  const activeMultipartOwnerParts =
189
297
  SchematicMultipartOwnerMatcher.collectActiveMultipartOwnerParts(
190
298
  recordIndexAwareRecords,
@@ -200,6 +308,7 @@ export class AltiumParser {
200
308
  AltiumParser.#isDrawableSchematicRecord(
201
309
  record.fields,
202
310
  ownersWithImplicitDisplayMode,
311
+ activeOwnerDisplayModes,
203
312
  activeMultipartOwnerParts
204
313
  )
205
314
  )
@@ -207,6 +316,7 @@ export class AltiumParser {
207
316
  AltiumParser.#isDrawableSchematicRecord(
208
317
  record.fields,
209
318
  ownersWithImplicitDisplayMode,
319
+ activeOwnerDisplayModes,
210
320
  activeMultipartOwnerParts
211
321
  )
212
322
  )
@@ -215,6 +325,7 @@ export class AltiumParser {
215
325
  AltiumParser.#isDrawableSchematicRecord(
216
326
  record.fields,
217
327
  ownersWithImplicitDisplayMode,
328
+ activeOwnerDisplayModes,
218
329
  activeMultipartOwnerParts
219
330
  ) &&
220
331
  getField(record.fields, 'RECORD') !== '211' &&
@@ -308,6 +419,9 @@ export class AltiumParser {
308
419
  const implementations = SchematicImplementationParser.parse(
309
420
  recordIndexAwareRecords
310
421
  )
422
+ const codeSymbols = SchematicCodeSymbolParser.parse(
423
+ recordIndexAwareRecords
424
+ )
311
425
  const displayModes = SchematicDisplayModeCatalogParser.parse(
312
426
  recordIndexAwareRecords
313
427
  )
@@ -394,7 +508,7 @@ export class AltiumParser {
394
508
  x2: parseNumericField(record.fields, 'Corner.X') || 0,
395
509
  y2: parseNumericField(record.fields, 'Corner.Y') || 0,
396
510
  color: toColor(record.fields.Color, '#a44a1b'),
397
- width: parseNumericField(record.fields, 'LineWidth') || 1,
511
+ width: parseSchematicLineWidth(record.fields),
398
512
  lineStyle: parseNumericField(record.fields, 'LineStyle') || 0,
399
513
  recordType: getField(record.fields, 'RECORD') || undefined,
400
514
  renderOrder:
@@ -503,6 +617,8 @@ export class AltiumParser {
503
617
  recordIndexAwareRecords,
504
618
  arrayBuffer
505
619
  )
620
+ const { thumbnails, diagnostics: thumbnailDiagnostics } =
621
+ SchematicThumbnailParser.parse(arrayBuffer)
506
622
  const template = SchematicTemplateParser.parse(
507
623
  recordIndexAwareRecords,
508
624
  sheetRecord,
@@ -616,12 +732,22 @@ export class AltiumParser {
616
732
  ports,
617
733
  crosses
618
734
  )
735
+ const componentDesignatorsByOwnerIndex =
736
+ AltiumParser.#componentDesignatorsByOwnerIndex(
737
+ recordIndexAwareRecords,
738
+ componentRecords,
739
+ components
740
+ )
619
741
 
620
742
  resolvedSheet.xZones =
621
743
  SchematicSheetStyleResolver.resolveXZones(resolvedSheet)
622
744
  resolvedSheet.yZones =
623
745
  SchematicSheetStyleResolver.resolveYZones(resolvedSheet)
624
746
  delete resolvedSheet.sheetStyle
747
+ const hyperlinks = SchematicHyperlinkParser.parse(
748
+ recordIndexAwareRecords,
749
+ resolvedSheet
750
+ )
625
751
 
626
752
  const title =
627
753
  AltiumParser.#findNamedText(textRecords, 'Title') ||
@@ -675,6 +801,7 @@ export class AltiumParser {
675
801
  }))
676
802
  )
677
803
  diagnostics.push(...imageDiagnostics)
804
+ diagnostics.push(...thumbnailDiagnostics)
678
805
  const { nets, diagnostics: netDiagnostics } =
679
806
  SchematicNetlistBuilder.build({
680
807
  lines: normalizedLines,
@@ -684,7 +811,8 @@ export class AltiumParser {
684
811
  crossSheetConnectors: crossSheetConnectors?.connectors || [],
685
812
  junctions,
686
813
  busEntries,
687
- sheetEntries
814
+ sheetEntries,
815
+ componentDesignatorsByOwnerIndex
688
816
  })
689
817
  AltiumParser.#suppressRedundantSinglePinPowerNetNames(
690
818
  pins,
@@ -700,12 +828,17 @@ export class AltiumParser {
700
828
  })
701
829
  const connectivityQa = SchematicConnectivityQaBuilder.build({
702
830
  nets,
831
+ lines: normalizedLines,
703
832
  texts: anchoredTexts,
704
833
  pins,
705
834
  ports,
706
835
  junctions
707
836
  })
708
837
  const embeddedFiles = schematicExtraction?.embeddedFiles || null
838
+ const nativeStreams = schematicExtraction?.nativeStreams || null
839
+ const opaqueRecords = Array.isArray(schematicExtraction?.opaqueRecords)
840
+ ? schematicExtraction.opaqueRecords
841
+ : []
709
842
 
710
843
  if (embeddedFiles?.diagnostics?.length) {
711
844
  diagnostics.push(
@@ -726,8 +859,17 @@ export class AltiumParser {
726
859
  componentCount: components.length,
727
860
  lineCount: lines.length,
728
861
  textCount: anchoredTexts.length,
862
+ ...(hyperlinks.length
863
+ ? { hyperlinkCount: hyperlinks.length }
864
+ : {}),
729
865
  recordTypeCount: recordTypes.length,
730
- bomRowCount: bom.length
866
+ bomRowCount: bom.length,
867
+ ...(nativeStreams?.summary?.streamCount
868
+ ? {
869
+ nativeStreamCount: nativeStreams.summary.streamCount
870
+ }
871
+ : {}),
872
+ opaqueRecordCount: opaqueRecords.length
731
873
  },
732
874
  diagnostics,
733
875
  schematic: {
@@ -750,6 +892,7 @@ export class AltiumParser {
750
892
  directiveSemantics,
751
893
  texts: anchoredTexts,
752
894
  textFrames,
895
+ ...(hyperlinks.length ? { hyperlinks } : {}),
753
896
  components,
754
897
  pins,
755
898
  ports,
@@ -762,11 +905,13 @@ export class AltiumParser {
762
905
  junctions,
763
906
  busEntries,
764
907
  images,
908
+ ...(thumbnails.length ? { thumbnails } : {}),
765
909
  nets,
766
910
  ownership,
767
911
  ...(template ? { template } : {}),
768
912
  ...(harnesses ? { harnesses } : {}),
769
913
  ...(implementations ? { implementations } : {}),
914
+ ...(codeSymbols ? { codeSymbols } : {}),
770
915
  ...(displayModes ? { displayModes } : {}),
771
916
  ...(bindings ? { bindings } : {}),
772
917
  ...(crossSheetConnectors ? { crossSheetConnectors } : {}),
@@ -776,6 +921,8 @@ export class AltiumParser {
776
921
  embeddedFiles.diagnostics?.length)
777
922
  ? { embeddedFiles }
778
923
  : {}),
924
+ ...(nativeStreams ? { nativeStreams } : {}),
925
+ ...(opaqueRecords.length ? { opaqueRecords } : {}),
779
926
  qa,
780
927
  connectivityQa
781
928
  },
@@ -783,6 +930,43 @@ export class AltiumParser {
783
930
  })
784
931
  }
785
932
 
933
+ /**
934
+ * Maps native component owner indexes to resolved component designators.
935
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Indexed schematic records.
936
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} componentRecords Component records.
937
+ * @param {{ designator?: string }[]} components Normalized components.
938
+ * @returns {Map<string, string>}
939
+ */
940
+ static #componentDesignatorsByOwnerIndex(
941
+ records,
942
+ componentRecords,
943
+ components
944
+ ) {
945
+ const designatorsByOwnerIndex = new Map()
946
+
947
+ for (let index = 0; index < componentRecords.length; index += 1) {
948
+ const designator = String(
949
+ components[index]?.designator || ''
950
+ ).trim()
951
+
952
+ if (!designator) {
953
+ continue
954
+ }
955
+
956
+ for (const ownerIndex of SchematicComponentOwnerTextResolver.resolveOwnerIndexes(
957
+ componentRecords[index],
958
+ records
959
+ )) {
960
+ const key = String(ownerIndex || '').trim()
961
+ if (key && !designatorsByOwnerIndex.has(key)) {
962
+ designatorsByOwnerIndex.set(key, designator)
963
+ }
964
+ }
965
+ }
966
+
967
+ return designatorsByOwnerIndex
968
+ }
969
+
786
970
  /**
787
971
  * Hides one-pin owner pin names when a connected power port already labels
788
972
  * the same net, preserving the numeric pin endpoint label.
@@ -826,7 +1010,7 @@ export class AltiumParser {
826
1010
  AltiumParser.#normalizePowerNetLabel(pin.name)
827
1011
  )
828
1012
  ) {
829
- pin.labelMode = 'number-only'
1013
+ AltiumParser.#setPinLabelMode(pin, pins, 'number-only')
830
1014
  }
831
1015
  }
832
1016
  }
@@ -850,11 +1034,51 @@ export class AltiumParser {
850
1034
  )
851
1035
 
852
1036
  if (matchesDirectPowerPort) {
853
- pin.labelMode = 'number-only'
1037
+ AltiumParser.#setPinLabelMode(pin, pins, 'number-only')
854
1038
  }
855
1039
  }
856
1040
  }
857
1041
 
1042
+ /**
1043
+ * Updates a net pin and its matching top-level pin when both are present.
1044
+ * @param {object} pin Net or top-level pin.
1045
+ * @param {object[]} pins Top-level pins.
1046
+ * @param {'hidden' | 'number-only' | 'name-only' | 'name-and-number'} labelMode New label mode.
1047
+ * @returns {void}
1048
+ */
1049
+ static #setPinLabelMode(pin, pins, labelMode) {
1050
+ pin.labelMode = labelMode
1051
+
1052
+ const sourcePin = (pins || []).find((candidate) =>
1053
+ AltiumParser.#pinsReferToSameSource(candidate, pin)
1054
+ )
1055
+
1056
+ if (sourcePin) {
1057
+ sourcePin.labelMode = labelMode
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Returns true when two pin rows describe the same native source pin.
1063
+ * @param {object} left First pin.
1064
+ * @param {object} right Second pin.
1065
+ * @returns {boolean}
1066
+ */
1067
+ static #pinsReferToSameSource(left, right) {
1068
+ if (left === right) {
1069
+ return true
1070
+ }
1071
+
1072
+ return (
1073
+ String(left?.ownerIndex || '') ===
1074
+ String(right?.ownerIndex || '') &&
1075
+ String(left?.designator || '') ===
1076
+ String(right?.designator || '') &&
1077
+ String(left?.name || '') === String(right?.name || '') &&
1078
+ AltiumParser.#pointsAreNear(left, right, 0.01)
1079
+ )
1080
+ }
1081
+
858
1082
  /**
859
1083
  * Returns true when a pin is the only pin on its owner and currently shows
860
1084
  * a name label.
@@ -962,6 +1186,103 @@ export class AltiumParser {
962
1186
  return owners
963
1187
  }
964
1188
 
1189
+ /**
1190
+ * Maps component owner indexes to their active display mode.
1191
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records
1192
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} componentRecords
1193
+ * @returns {Map<string, number>}
1194
+ */
1195
+ static #collectActiveOwnerDisplayModes(records, componentRecords) {
1196
+ const activeDisplayModes = new Map()
1197
+
1198
+ for (const componentRecord of componentRecords) {
1199
+ const displayMode =
1200
+ parseNumericField(componentRecord.fields, 'DisplayMode') || 1
1201
+ const displayModeCount =
1202
+ parseNumericField(componentRecord.fields, 'DisplayModeCount') ||
1203
+ 0
1204
+ const ownerIndexes = AltiumParser.#componentOwnerIndexCandidates(
1205
+ records,
1206
+ componentRecord
1207
+ )
1208
+
1209
+ if (displayModeCount <= 1 && !ownerIndexes.size) {
1210
+ continue
1211
+ }
1212
+
1213
+ for (const ownerIndex of ownerIndexes) {
1214
+ activeDisplayModes.set(ownerIndex, displayMode)
1215
+ }
1216
+ }
1217
+
1218
+ return activeDisplayModes
1219
+ }
1220
+
1221
+ /**
1222
+ * Resolves owner indexes that may point at one component placement.
1223
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records
1224
+ * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} componentRecord
1225
+ * @returns {Set<string>}
1226
+ */
1227
+ static #componentOwnerIndexCandidates(records, componentRecord) {
1228
+ const ownerIndexes = new Set()
1229
+ const indexInSheet = parseNumericField(
1230
+ componentRecord.fields,
1231
+ 'IndexInSheet'
1232
+ )
1233
+
1234
+ if (indexInSheet !== null) {
1235
+ ownerIndexes.add(String(indexInSheet))
1236
+ }
1237
+ if (Number.isInteger(componentRecord.recordIndex)) {
1238
+ ownerIndexes.add(String(componentRecord.recordIndex))
1239
+ }
1240
+
1241
+ for (const ownerIndex of AltiumParser.#serializedOwnerDisplayModeIndexes(
1242
+ records,
1243
+ componentRecord.recordIndex
1244
+ )) {
1245
+ ownerIndexes.add(ownerIndex)
1246
+ }
1247
+
1248
+ return ownerIndexes
1249
+ }
1250
+
1251
+ /**
1252
+ * Collects display-mode owner indexes serialized after one component record.
1253
+ * @param {{ fields: Record<string, string | string[]> }[]} records
1254
+ * @param {number | undefined} componentRecordIndex
1255
+ * @returns {Set<string>}
1256
+ */
1257
+ static #serializedOwnerDisplayModeIndexes(records, componentRecordIndex) {
1258
+ const ownerIndexes = new Set()
1259
+ if (!Number.isInteger(componentRecordIndex)) {
1260
+ return ownerIndexes
1261
+ }
1262
+
1263
+ for (
1264
+ let index = componentRecordIndex + 1;
1265
+ index < records.length;
1266
+ index += 1
1267
+ ) {
1268
+ const record = records[index]
1269
+ if (getField(record.fields, 'RECORD') === '1') {
1270
+ break
1271
+ }
1272
+
1273
+ if (!getField(record.fields, 'OwnerPartDisplayMode')) {
1274
+ continue
1275
+ }
1276
+
1277
+ const ownerIndex = getField(record.fields, 'OwnerIndex')
1278
+ if (ownerIndex) {
1279
+ ownerIndexes.add(ownerIndex)
1280
+ }
1281
+ }
1282
+
1283
+ return ownerIndexes
1284
+ }
1285
+
965
1286
  /**
966
1287
  * Collects symbol owners that draw an inaccessible body around compact
967
1288
  * internal pins.
@@ -1024,19 +1345,33 @@ export class AltiumParser {
1024
1345
  * display mode for its owner.
1025
1346
  * @param {Record<string, string | string[]>} fields
1026
1347
  * @param {Set<string>} ownersWithImplicitDisplayMode
1348
+ * @param {Map<string, number>} activeOwnerDisplayModes
1027
1349
  * @returns {boolean}
1028
1350
  */
1029
1351
  static #isActiveSchematicDisplayModeRecord(
1030
1352
  fields,
1031
- ownersWithImplicitDisplayMode
1353
+ ownersWithImplicitDisplayMode,
1354
+ activeOwnerDisplayModes
1032
1355
  ) {
1033
1356
  const ownerIndex = getField(fields, 'OwnerIndex')
1034
- const ownerPartDisplayMode = getField(fields, 'OwnerPartDisplayMode')
1035
- if (!ownerIndex || !ownerPartDisplayMode) {
1357
+ const ownerPartDisplayMode = parseNumericField(
1358
+ fields,
1359
+ 'OwnerPartDisplayMode'
1360
+ )
1361
+ if (!ownerIndex || ownerPartDisplayMode === null) {
1036
1362
  return true
1037
1363
  }
1038
1364
 
1039
- return !ownersWithImplicitDisplayMode.has(ownerIndex)
1365
+ if (ownersWithImplicitDisplayMode.has(ownerIndex)) {
1366
+ return false
1367
+ }
1368
+
1369
+ const activeDisplayMode = activeOwnerDisplayModes.get(ownerIndex)
1370
+ if (activeDisplayMode !== undefined) {
1371
+ return ownerPartDisplayMode === activeDisplayMode
1372
+ }
1373
+
1374
+ return true
1040
1375
  }
1041
1376
 
1042
1377
  /**
@@ -1044,18 +1379,21 @@ export class AltiumParser {
1044
1379
  * display mode and the active multipart owner part for its owner.
1045
1380
  * @param {Record<string, string | string[]>} fields
1046
1381
  * @param {Set<string>} ownersWithImplicitDisplayMode
1382
+ * @param {Map<string, number>} activeOwnerDisplayModes
1047
1383
  * @param {Map<string, string>} activeMultipartOwnerParts
1048
1384
  * @returns {boolean}
1049
1385
  */
1050
1386
  static #isDrawableSchematicRecord(
1051
1387
  fields,
1052
1388
  ownersWithImplicitDisplayMode,
1389
+ activeOwnerDisplayModes,
1053
1390
  activeMultipartOwnerParts
1054
1391
  ) {
1055
1392
  return (
1056
1393
  AltiumParser.#isActiveSchematicDisplayModeRecord(
1057
1394
  fields,
1058
- ownersWithImplicitDisplayMode
1395
+ ownersWithImplicitDisplayMode,
1396
+ activeOwnerDisplayModes
1059
1397
  ) &&
1060
1398
  SchematicMultipartOwnerMatcher.isActiveOwnerPartRecord(
1061
1399
  fields,