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
@@ -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' &&
@@ -238,8 +349,11 @@ export class AltiumParser {
238
349
  const rectangleRecords = drawableRecords.filter(
239
350
  (record) =>
240
351
  SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
241
- AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
242
- AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
352
+ ((AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
353
+ AltiumParser.#hasCoordinatePair(record.fields, 'Corner')) ||
354
+ SchematicPrimitiveParser.isPointListedRectangleRecord(
355
+ record.fields
356
+ ))
243
357
  )
244
358
  const roundedRectangleRecords = drawableRecords.filter(
245
359
  (record) =>
@@ -305,6 +419,9 @@ export class AltiumParser {
305
419
  const implementations = SchematicImplementationParser.parse(
306
420
  recordIndexAwareRecords
307
421
  )
422
+ const codeSymbols = SchematicCodeSymbolParser.parse(
423
+ recordIndexAwareRecords
424
+ )
308
425
  const displayModes = SchematicDisplayModeCatalogParser.parse(
309
426
  recordIndexAwareRecords
310
427
  )
@@ -391,7 +508,7 @@ export class AltiumParser {
391
508
  x2: parseNumericField(record.fields, 'Corner.X') || 0,
392
509
  y2: parseNumericField(record.fields, 'Corner.Y') || 0,
393
510
  color: toColor(record.fields.Color, '#a44a1b'),
394
- width: parseNumericField(record.fields, 'LineWidth') || 1,
511
+ width: parseSchematicLineWidth(record.fields),
395
512
  lineStyle: parseNumericField(record.fields, 'LineStyle') || 0,
396
513
  recordType: getField(record.fields, 'RECORD') || undefined,
397
514
  renderOrder:
@@ -500,6 +617,8 @@ export class AltiumParser {
500
617
  recordIndexAwareRecords,
501
618
  arrayBuffer
502
619
  )
620
+ const { thumbnails, diagnostics: thumbnailDiagnostics } =
621
+ SchematicThumbnailParser.parse(arrayBuffer)
503
622
  const template = SchematicTemplateParser.parse(
504
623
  recordIndexAwareRecords,
505
624
  sheetRecord,
@@ -613,12 +732,22 @@ export class AltiumParser {
613
732
  ports,
614
733
  crosses
615
734
  )
735
+ const componentDesignatorsByOwnerIndex =
736
+ AltiumParser.#componentDesignatorsByOwnerIndex(
737
+ recordIndexAwareRecords,
738
+ componentRecords,
739
+ components
740
+ )
616
741
 
617
742
  resolvedSheet.xZones =
618
743
  SchematicSheetStyleResolver.resolveXZones(resolvedSheet)
619
744
  resolvedSheet.yZones =
620
745
  SchematicSheetStyleResolver.resolveYZones(resolvedSheet)
621
746
  delete resolvedSheet.sheetStyle
747
+ const hyperlinks = SchematicHyperlinkParser.parse(
748
+ recordIndexAwareRecords,
749
+ resolvedSheet
750
+ )
622
751
 
623
752
  const title =
624
753
  AltiumParser.#findNamedText(textRecords, 'Title') ||
@@ -672,6 +801,7 @@ export class AltiumParser {
672
801
  }))
673
802
  )
674
803
  diagnostics.push(...imageDiagnostics)
804
+ diagnostics.push(...thumbnailDiagnostics)
675
805
  const { nets, diagnostics: netDiagnostics } =
676
806
  SchematicNetlistBuilder.build({
677
807
  lines: normalizedLines,
@@ -681,7 +811,8 @@ export class AltiumParser {
681
811
  crossSheetConnectors: crossSheetConnectors?.connectors || [],
682
812
  junctions,
683
813
  busEntries,
684
- sheetEntries
814
+ sheetEntries,
815
+ componentDesignatorsByOwnerIndex
685
816
  })
686
817
  AltiumParser.#suppressRedundantSinglePinPowerNetNames(
687
818
  pins,
@@ -697,12 +828,17 @@ export class AltiumParser {
697
828
  })
698
829
  const connectivityQa = SchematicConnectivityQaBuilder.build({
699
830
  nets,
831
+ lines: normalizedLines,
700
832
  texts: anchoredTexts,
701
833
  pins,
702
834
  ports,
703
835
  junctions
704
836
  })
705
837
  const embeddedFiles = schematicExtraction?.embeddedFiles || null
838
+ const nativeStreams = schematicExtraction?.nativeStreams || null
839
+ const opaqueRecords = Array.isArray(schematicExtraction?.opaqueRecords)
840
+ ? schematicExtraction.opaqueRecords
841
+ : []
706
842
 
707
843
  if (embeddedFiles?.diagnostics?.length) {
708
844
  diagnostics.push(
@@ -723,8 +859,17 @@ export class AltiumParser {
723
859
  componentCount: components.length,
724
860
  lineCount: lines.length,
725
861
  textCount: anchoredTexts.length,
862
+ ...(hyperlinks.length
863
+ ? { hyperlinkCount: hyperlinks.length }
864
+ : {}),
726
865
  recordTypeCount: recordTypes.length,
727
- bomRowCount: bom.length
866
+ bomRowCount: bom.length,
867
+ ...(nativeStreams?.summary?.streamCount
868
+ ? {
869
+ nativeStreamCount: nativeStreams.summary.streamCount
870
+ }
871
+ : {}),
872
+ opaqueRecordCount: opaqueRecords.length
728
873
  },
729
874
  diagnostics,
730
875
  schematic: {
@@ -747,6 +892,7 @@ export class AltiumParser {
747
892
  directiveSemantics,
748
893
  texts: anchoredTexts,
749
894
  textFrames,
895
+ ...(hyperlinks.length ? { hyperlinks } : {}),
750
896
  components,
751
897
  pins,
752
898
  ports,
@@ -759,11 +905,13 @@ export class AltiumParser {
759
905
  junctions,
760
906
  busEntries,
761
907
  images,
908
+ ...(thumbnails.length ? { thumbnails } : {}),
762
909
  nets,
763
910
  ownership,
764
911
  ...(template ? { template } : {}),
765
912
  ...(harnesses ? { harnesses } : {}),
766
913
  ...(implementations ? { implementations } : {}),
914
+ ...(codeSymbols ? { codeSymbols } : {}),
767
915
  ...(displayModes ? { displayModes } : {}),
768
916
  ...(bindings ? { bindings } : {}),
769
917
  ...(crossSheetConnectors ? { crossSheetConnectors } : {}),
@@ -773,6 +921,8 @@ export class AltiumParser {
773
921
  embeddedFiles.diagnostics?.length)
774
922
  ? { embeddedFiles }
775
923
  : {}),
924
+ ...(nativeStreams ? { nativeStreams } : {}),
925
+ ...(opaqueRecords.length ? { opaqueRecords } : {}),
776
926
  qa,
777
927
  connectivityQa
778
928
  },
@@ -780,6 +930,43 @@ export class AltiumParser {
780
930
  })
781
931
  }
782
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
+
783
970
  /**
784
971
  * Hides one-pin owner pin names when a connected power port already labels
785
972
  * the same net, preserving the numeric pin endpoint label.
@@ -823,7 +1010,7 @@ export class AltiumParser {
823
1010
  AltiumParser.#normalizePowerNetLabel(pin.name)
824
1011
  )
825
1012
  ) {
826
- pin.labelMode = 'number-only'
1013
+ AltiumParser.#setPinLabelMode(pin, pins, 'number-only')
827
1014
  }
828
1015
  }
829
1016
  }
@@ -847,11 +1034,51 @@ export class AltiumParser {
847
1034
  )
848
1035
 
849
1036
  if (matchesDirectPowerPort) {
850
- pin.labelMode = 'number-only'
1037
+ AltiumParser.#setPinLabelMode(pin, pins, 'number-only')
851
1038
  }
852
1039
  }
853
1040
  }
854
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
+
855
1082
  /**
856
1083
  * Returns true when a pin is the only pin on its owner and currently shows
857
1084
  * a name label.
@@ -959,6 +1186,103 @@ export class AltiumParser {
959
1186
  return owners
960
1187
  }
961
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
+
962
1286
  /**
963
1287
  * Collects symbol owners that draw an inaccessible body around compact
964
1288
  * internal pins.
@@ -1021,19 +1345,33 @@ export class AltiumParser {
1021
1345
  * display mode for its owner.
1022
1346
  * @param {Record<string, string | string[]>} fields
1023
1347
  * @param {Set<string>} ownersWithImplicitDisplayMode
1348
+ * @param {Map<string, number>} activeOwnerDisplayModes
1024
1349
  * @returns {boolean}
1025
1350
  */
1026
1351
  static #isActiveSchematicDisplayModeRecord(
1027
1352
  fields,
1028
- ownersWithImplicitDisplayMode
1353
+ ownersWithImplicitDisplayMode,
1354
+ activeOwnerDisplayModes
1029
1355
  ) {
1030
1356
  const ownerIndex = getField(fields, 'OwnerIndex')
1031
- const ownerPartDisplayMode = getField(fields, 'OwnerPartDisplayMode')
1032
- if (!ownerIndex || !ownerPartDisplayMode) {
1357
+ const ownerPartDisplayMode = parseNumericField(
1358
+ fields,
1359
+ 'OwnerPartDisplayMode'
1360
+ )
1361
+ if (!ownerIndex || ownerPartDisplayMode === null) {
1033
1362
  return true
1034
1363
  }
1035
1364
 
1036
- 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
1037
1375
  }
1038
1376
 
1039
1377
  /**
@@ -1041,18 +1379,21 @@ export class AltiumParser {
1041
1379
  * display mode and the active multipart owner part for its owner.
1042
1380
  * @param {Record<string, string | string[]>} fields
1043
1381
  * @param {Set<string>} ownersWithImplicitDisplayMode
1382
+ * @param {Map<string, number>} activeOwnerDisplayModes
1044
1383
  * @param {Map<string, string>} activeMultipartOwnerParts
1045
1384
  * @returns {boolean}
1046
1385
  */
1047
1386
  static #isDrawableSchematicRecord(
1048
1387
  fields,
1049
1388
  ownersWithImplicitDisplayMode,
1389
+ activeOwnerDisplayModes,
1050
1390
  activeMultipartOwnerParts
1051
1391
  ) {
1052
1392
  return (
1053
1393
  AltiumParser.#isActiveSchematicDisplayModeRecord(
1054
1394
  fields,
1055
- ownersWithImplicitDisplayMode
1395
+ ownersWithImplicitDisplayMode,
1396
+ activeOwnerDisplayModes
1056
1397
  ) &&
1057
1398
  SchematicMultipartOwnerMatcher.isActiveOwnerPartRecord(
1058
1399
  fields,