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
@@ -53,6 +53,50 @@ export class PcbLayerStackSourceMetadataParser {
53
53
  'COVERLAYEXPANSION',
54
54
  'COVERLAY_EXPANSION'
55
55
  ]),
56
+ process: layerField(['PROCESS']),
57
+ pullbackDistance: layerField([
58
+ 'PULLBACKDISTANCE',
59
+ 'PULLBACK_DISTANCE'
60
+ ]),
61
+ copperOrientation: layerField([
62
+ 'COPPERORIENTATION',
63
+ 'COPPER_ORIENTATION'
64
+ ]),
65
+ orientation: layerField(['ORIENTATION']),
66
+ note: layerField(['NOTE']),
67
+ comment: layerField(['COMMENT']),
68
+ materialManufacturer: layerField([
69
+ 'MATERIALMANUFACTURER',
70
+ 'MATERIAL_MANUFACTURER'
71
+ ]),
72
+ materialDescription: layerField([
73
+ 'MATERIALDESCRIPTION',
74
+ 'MATERIAL_DESCRIPTION'
75
+ ]),
76
+ materialGlassTransitionTemp: layerField([
77
+ 'MATERIALGLASSTRANSITIONTEMP',
78
+ 'MATERIAL_GLASS_TRANSITION_TEMP'
79
+ ]),
80
+ glassTransitionTemp: layerField([
81
+ 'GLASSTRANSITIONTEMP',
82
+ 'GLASS_TRANSITION_TEMP'
83
+ ]),
84
+ dielectricStrength: layerField([
85
+ 'DIELECTRICSTRENGTH',
86
+ 'DIELECTRIC_STRENGTH'
87
+ ]),
88
+ volumeResistivity: layerField([
89
+ 'VOLUMERESISTIVITY',
90
+ 'VOLUME_RESISTIVITY'
91
+ ]),
92
+ resin: layerField(['RESIN']),
93
+ solid: layerField(['SOLID']),
94
+ materialFrequency: layerField([
95
+ 'MATERIALFREQUENCY',
96
+ 'MATERIAL_FREQUENCY'
97
+ ]),
98
+ frequency: layerField(['FREQUENCY']),
99
+ constructions: layerField(['CONSTRUCTIONS']),
56
100
  isStiffener: PcbLayerStackSourceMetadataParser.#optionalBoolean(
57
101
  layerField(['ISSTIFFENER', 'IS_STIFFENER'])
58
102
  ),
@@ -235,10 +279,17 @@ export class PcbLayerStackSourceMetadataParser {
235
279
  * @returns {object[]}
236
280
  */
237
281
  static #substackEnablement(fields, layerIndex) {
238
- const pattern = new RegExp(
239
- '^V9_STACK_LAYER' + layerIndex + '_SUBSTACK(\\d+)_ENABLED$',
240
- 'iu'
241
- )
282
+ const prefix = 'V9_STACK_LAYER' + layerIndex + '_SUBSTACK'
283
+ if (
284
+ !PcbLayerStackSourceMetadataParser.#hasCaseInsensitivePrefix(
285
+ fields,
286
+ prefix
287
+ )
288
+ ) {
289
+ return []
290
+ }
291
+
292
+ const pattern = new RegExp('^' + prefix + '(\\d+)_ENABLED$', 'iu')
242
293
 
243
294
  return Object.keys(fields)
244
295
  .flatMap((key) => {
@@ -261,6 +312,25 @@ export class PcbLayerStackSourceMetadataParser {
261
312
  .sort((left, right) => left.substackIndex - right.substackIndex)
262
313
  }
263
314
 
315
+ /**
316
+ * Returns true when any field key starts with a prefix, ignoring case.
317
+ * @param {Record<string, string | string[]>} fields Source fields.
318
+ * @param {string} prefix Field-key prefix.
319
+ * @returns {boolean}
320
+ */
321
+ static #hasCaseInsensitivePrefix(fields, prefix) {
322
+ const upperPrefix = prefix.toUpperCase()
323
+ for (const key of PcbLayerStackSourceMetadataParser.#fieldIndex(
324
+ fields
325
+ ).keys()) {
326
+ if (key.startsWith(upperPrefix)) {
327
+ return true
328
+ }
329
+ }
330
+
331
+ return false
332
+ }
333
+
264
334
  /**
265
335
  * Parses branch-section stack rows.
266
336
  * @param {Record<string, string | string[]>} fields Board fields.
@@ -20,7 +20,7 @@ export class PcbLibModelParser {
20
20
  /**
21
21
  * Parses one extracted PcbLib into a normalized read-only model.
22
22
  * @param {string} fileName
23
- * @param {{ libraryHeader?: Record<string, string>, componentParamsToc?: Record<string, object>, sectionKeys?: Record<string, string>, footprints?: object[], embeddedFonts?: { fonts?: object[] }, streamNames?: string[], diagnostics?: Record<string, number> } | null} extraction
23
+ * @param {{ libraryHeader?: Record<string, string>, componentParamsToc?: Record<string, object>, sectionKeys?: Record<string, string>, footprints?: object[], embeddedFonts?: { fonts?: object[] }, nativeStreams?: object, streamNames?: string[], diagnostics?: Record<string, number> } | null} extraction
24
24
  * @returns {{ schema: string, kind: 'pcb-library', fileType: 'PcbLib', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], pcbLibrary: { libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], embeddedFonts: object[] }, bom: [] }}
25
25
  */
26
26
  static parse(fileName, extraction) {
@@ -54,7 +54,8 @@ export class PcbLibModelParser {
54
54
  fileName,
55
55
  footprints,
56
56
  embeddedFonts,
57
- embeddedModels
57
+ embeddedModels,
58
+ safeExtraction.nativeStreams
58
59
  )
59
60
  const diagnostics = PcbLibModelParser.#buildDiagnostics(
60
61
  footprints,
@@ -71,6 +72,9 @@ export class PcbLibModelParser {
71
72
  embeddedFonts,
72
73
  embeddedModels,
73
74
  componentBodies,
75
+ ...(safeExtraction.nativeStreams
76
+ ? { nativeStreams: safeExtraction.nativeStreams }
77
+ : {}),
74
78
  ...(defaults ? { defaults } : {})
75
79
  }
76
80
  pcbLibrary.renderManifest =
@@ -94,9 +98,16 @@ export class PcbLibModelParser {
94
98
  * @param {object[]} footprints
95
99
  * @param {object[]} embeddedFonts
96
100
  * @param {object[]} embeddedModels
101
+ * @param {object | undefined} nativeStreams Native stream inventory.
97
102
  * @returns {Record<string, number | string>}
98
103
  */
99
- static #buildSummary(fileName, footprints, embeddedFonts, embeddedModels) {
104
+ static #buildSummary(
105
+ fileName,
106
+ footprints,
107
+ embeddedFonts,
108
+ embeddedModels,
109
+ nativeStreams
110
+ ) {
100
111
  return {
101
112
  title: stripExtension(fileName),
102
113
  footprintCount: footprints.length,
@@ -116,7 +127,12 @@ export class PcbLibModelParser {
116
127
  'rawRecords'
117
128
  ),
118
129
  embeddedFontCount: embeddedFonts.length,
119
- embeddedModelCount: embeddedModels.length
130
+ embeddedModelCount: embeddedModels.length,
131
+ ...(nativeStreams?.summary?.streamCount
132
+ ? {
133
+ nativeStreamCount: nativeStreams.summary.streamCount
134
+ }
135
+ : {})
120
136
  }
121
137
  }
122
138
 
@@ -7,6 +7,7 @@ import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
7
7
  import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
8
8
  import { PcbEmbeddedModelExtractor } from './PcbEmbeddedModelExtractor.mjs'
9
9
  import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
10
+ import { NativeStreamInventoryBuilder } from './NativeStreamInventoryBuilder.mjs'
10
11
  import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
11
12
  import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
12
13
  import { OleConstants } from '../ole/OleConstants.mjs'
@@ -164,18 +165,29 @@ export class PcbLibStreamExtractor {
164
165
  PcbEmbeddedFontExtractor.extractFromStreams(streams)
165
166
  const embeddedModels =
166
167
  PcbEmbeddedModelExtractor.extractFromStreams(streams)
168
+ const usedStreamNames = PcbLibStreamExtractor.#collectUsedStreamNames(
169
+ footprints,
170
+ streams,
171
+ embeddedFonts,
172
+ embeddedModels
173
+ )
174
+ const nativeStreams = NativeStreamInventoryBuilder.buildFromStreams(
175
+ streams,
176
+ {
177
+ source: 'pcblib',
178
+ consumedStreamNames: usedStreamNames,
179
+ knownStreamNames:
180
+ PcbLibStreamExtractor.#knownStreamNames(streams)
181
+ }
182
+ )
167
183
 
168
184
  return {
169
185
  libraryHeader: parsedLibraryData.libraryHeader,
170
186
  componentParamsToc,
171
187
  sectionKeys,
172
188
  footprints,
173
- streamNames: PcbLibStreamExtractor.#collectUsedStreamNames(
174
- footprints,
175
- streams,
176
- embeddedFonts,
177
- embeddedModels
178
- ),
189
+ streamNames: usedStreamNames,
190
+ nativeStreams,
179
191
  embeddedFonts,
180
192
  embeddedModels,
181
193
  diagnostics: {
@@ -999,6 +1011,37 @@ export class PcbLibStreamExtractor {
999
1011
  return [...names].sort((left, right) => left.localeCompare(right))
1000
1012
  }
1001
1013
 
1014
+ /**
1015
+ * Builds recognized PcbLib stream names from one stream map.
1016
+ * @param {Map<string, Uint8Array>} streams Native stream map.
1017
+ * @returns {string[]}
1018
+ */
1019
+ static #knownStreamNames(streams) {
1020
+ return [...(streams || new Map()).keys()].filter((streamName) => {
1021
+ if (
1022
+ [
1023
+ 'Library/Data',
1024
+ 'Library/ComponentParamsTOC/Data',
1025
+ 'SectionKeys'
1026
+ ].includes(streamName)
1027
+ ) {
1028
+ return true
1029
+ }
1030
+
1031
+ const leafName = String(streamName || '')
1032
+ .split('/')
1033
+ .at(-1)
1034
+ return [
1035
+ 'Header',
1036
+ 'Data',
1037
+ 'Parameters',
1038
+ 'WideStrings',
1039
+ 'ExtendedPrimitiveInformation',
1040
+ 'CustomShapes'
1041
+ ].includes(leafName)
1042
+ })
1043
+ }
1044
+
1002
1045
  /**
1003
1046
  * Sanitizes one footprint name for legacy OLE storage lookup.
1004
1047
  * @param {string} name
@@ -12,6 +12,7 @@ import { PcbComponentKindPolicy } from './PcbComponentKindPolicy.mjs'
12
12
  import { PcbComponentPrimitiveIndexer } from './PcbComponentPrimitiveIndexer.mjs'
13
13
  import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
14
14
  import { PcbDimensionParser } from './PcbDimensionParser.mjs'
15
+ import { PcbLayerStackCustomDataParser } from './PcbLayerStackCustomDataParser.mjs'
15
16
  import { PcbLayerStackReadModelBuilder } from './PcbLayerStackReadModelBuilder.mjs'
16
17
  import { PcbMechanicalLayerPairParser } from './PcbMechanicalLayerPairParser.mjs'
17
18
  import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
@@ -50,13 +51,21 @@ export class PcbModelParser {
50
51
  * resolved primitive netName fields keyed by numeric netIndex values.
51
52
  * @param {string} fileName
52
53
  * @param {{ raw: string, fields: Record<string, string | string[]>, sourceStream?: string }[]} records
53
- * @param {{ streamNames: string[], binaryPrimitives: Record<string, object[]>, primitiveParameters?: object, viaStructures?: object, customPadShapes?: object, extendedPrimitiveInformation?: object, unions?: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number } } | null} pcbExtraction
54
+ * @param {{ streamNames: string[], nativeStreams?: object, binaryPrimitives: Record<string, object[]>, primitiveParameters?: object, viaStructures?: object, customPadShapes?: object, extendedPrimitiveInformation?: object, unions?: object, diagnostics: { printableRecordCount: number, printableStreamCount: number, binaryPrimitiveCount: number } } | null} pcbExtraction
54
55
  * @returns {{ schema: string, kind: 'pcb', fileType: 'PcbDoc', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], pcb: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
55
56
  */
56
57
  static parse(fileName, records, pcbExtraction = null) {
57
58
  const boardRecords = records.filter(
58
59
  (record) => record.sourceStream === 'Board6/Data'
59
60
  )
61
+ const stackCustomData =
62
+ PcbLayerStackCustomDataParser.parseBoardRecordsWithDiagnostics(
63
+ boardRecords
64
+ )
65
+ const layerStackBoardRecords = [
66
+ ...boardRecords,
67
+ ...stackCustomData.records
68
+ ]
60
69
  const boardRecord =
61
70
  boardRecords.find(
62
71
  (record) =>
@@ -64,7 +73,7 @@ export class PcbModelParser {
64
73
  record.sourceStream === 'Board6/Data'
65
74
  ) || records.find((record) => getField(record.fields, 'KIND0'))
66
75
  const layerRecord =
67
- boardRecords.find(
76
+ layerStackBoardRecords.find(
68
77
  (record) =>
69
78
  countMatchingKeys(
70
79
  record.fields,
@@ -100,7 +109,7 @@ export class PcbModelParser {
100
109
  )
101
110
  const layerSubstacks =
102
111
  PcbBoardRegionSemanticsParser.parseLayerSubstacks(
103
- boardRecords.map((record) => record.fields)
112
+ layerStackBoardRecords.map((record) => record.fields)
104
113
  )
105
114
  const primitiveLayers = AltiumLayoutParser.parsePrimitiveLayerNames(
106
115
  boardRecords.map((record) => record.fields)
@@ -120,6 +129,9 @@ export class PcbModelParser {
120
129
  PcbModelParser.#parseDifferentialPairRecords(records),
121
130
  classes
122
131
  )
132
+ const embeddedBoards =
133
+ PcbModelParser.#parseEmbeddedBoardRecords(records)
134
+ const rooms = PcbModelParser.#parseRoomRecords(records)
123
135
  const rules = PcbRuleParser.parse(records)
124
136
  const defaults = PcbDefaultsParser.parse(
125
137
  boardRecord?.fields || {},
@@ -190,6 +202,7 @@ export class PcbModelParser {
190
202
  const rawRecords = Array.isArray(pcbExtraction?.rawRecords)
191
203
  ? pcbExtraction.rawRecords
192
204
  : []
205
+ const nativeStreams = pcbExtraction?.nativeStreams || null
193
206
  const viaStructures = pcbExtraction?.viaStructures || {
194
207
  structures: [],
195
208
  links: [],
@@ -257,7 +270,7 @@ export class PcbModelParser {
257
270
  )
258
271
  const layerStackReadModel = PcbLayerStackReadModelBuilder.build({
259
272
  fileName,
260
- boardRecords,
273
+ boardRecords: layerStackBoardRecords,
261
274
  streamNames: pcbExtraction?.streamNames || [],
262
275
  layers,
263
276
  primitiveLayers,
@@ -406,6 +419,36 @@ export class PcbModelParser {
406
419
  })
407
420
  }
408
421
 
422
+ if (embeddedBoards.length || rooms.length) {
423
+ diagnostics.push({
424
+ severity: 'info',
425
+ message:
426
+ 'Recovered ' +
427
+ embeddedBoards.length +
428
+ ' embedded board ' +
429
+ PcbModelParser.#plural(
430
+ embeddedBoards.length,
431
+ 'placement',
432
+ 'placements'
433
+ ) +
434
+ ' and ' +
435
+ rooms.length +
436
+ ' placement ' +
437
+ PcbModelParser.#plural(rooms.length, 'room', 'rooms') +
438
+ '.'
439
+ })
440
+ }
441
+
442
+ for (const issue of stackCustomData.diagnostics) {
443
+ diagnostics.push({
444
+ severity: issue.severity || 'warning',
445
+ code: issue.code,
446
+ message: issue.message,
447
+ sourceStream: issue.sourceStream,
448
+ fieldName: issue.fieldName
449
+ })
450
+ }
451
+
409
452
  for (const issue of layerStackReadModel?.diagnostics || []) {
410
453
  diagnostics.push({
411
454
  severity: issue.severity || 'warning',
@@ -607,6 +650,10 @@ export class PcbModelParser {
607
650
  differentialPairData.differentialPairs.length,
608
651
  differentialPairClassCount:
609
652
  differentialPairData.differentialPairClasses.length,
653
+ ...(embeddedBoards.length
654
+ ? { embeddedBoardCount: embeddedBoards.length }
655
+ : {}),
656
+ ...(rooms.length ? { roomCount: rooms.length } : {}),
610
657
  ruleCount: rules.length,
611
658
  routeReviewGroupCount:
612
659
  reviewMetadata.summary.routeGroupCount || 0,
@@ -649,6 +696,11 @@ export class PcbModelParser {
649
696
  embeddedFontCount: extractedEmbeddedFonts.length,
650
697
  embeddedFileCount: embeddedFiles.files.length,
651
698
  rawRecordCount: rawRecords.length,
699
+ ...(nativeStreams?.summary?.streamCount
700
+ ? {
701
+ nativeStreamCount: nativeStreams.summary.streamCount
702
+ }
703
+ : {}),
652
704
  boardWidthMil: Math.round(boardOutline.widthMil),
653
705
  boardHeightMil: Math.round(boardOutline.heightMil)
654
706
  },
@@ -669,6 +721,8 @@ export class PcbModelParser {
669
721
  differentialPairs: differentialPairData.differentialPairs,
670
722
  differentialPairClasses:
671
723
  differentialPairData.differentialPairClasses,
724
+ ...(embeddedBoards.length ? { embeddedBoards } : {}),
725
+ ...(rooms.length ? { rooms } : {}),
672
726
  rules,
673
727
  ...(defaults ? { defaults } : {}),
674
728
  maskPaste,
@@ -698,6 +752,7 @@ export class PcbModelParser {
698
752
  embeddedFonts: extractedEmbeddedFonts,
699
753
  embeddedFiles,
700
754
  rawRecords,
755
+ ...(nativeStreams ? { nativeStreams } : {}),
701
756
  componentBodies,
702
757
  componentPrimitives,
703
758
  componentPrimitiveGroups,
@@ -995,6 +1050,136 @@ export class PcbModelParser {
995
1050
  return netNameByIndex
996
1051
  }
997
1052
 
1053
+ /**
1054
+ * Parses panel embedded-board placement records.
1055
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records Source records.
1056
+ * @returns {object[]}
1057
+ */
1058
+ static #parseEmbeddedBoardRecords(records) {
1059
+ return (records || [])
1060
+ .filter((record) => record.sourceStream === 'EmbeddedBoards6/Data')
1061
+ .map((record, index) =>
1062
+ PcbModelParser.#normalizeEmbeddedBoardRecord(
1063
+ record.fields,
1064
+ index
1065
+ )
1066
+ )
1067
+ .filter(
1068
+ (embeddedBoard) =>
1069
+ embeddedBoard.documentPath || embeddedBoard.uniqueId
1070
+ )
1071
+ }
1072
+
1073
+ /**
1074
+ * Normalizes one embedded-board placement.
1075
+ * @param {Record<string, string | string[]>} fields Native fields.
1076
+ * @param {number} embeddedBoardIndex Fallback index.
1077
+ * @returns {object}
1078
+ */
1079
+ static #normalizeEmbeddedBoardRecord(fields, embeddedBoardIndex) {
1080
+ const documentPath = PcbModelParser.#firstField(fields, [
1081
+ 'DOCUMENTPATH',
1082
+ 'DOCUMENTNAME',
1083
+ 'FILENAME'
1084
+ ])
1085
+ const array = PcbModelParser.#stripEmptyObject({
1086
+ columns: PcbModelParser.#firstNumberField(fields, [
1087
+ 'COLCOUNT',
1088
+ 'COLUMNCOUNT',
1089
+ 'COLUMNS'
1090
+ ]),
1091
+ rows: PcbModelParser.#firstNumberField(fields, [
1092
+ 'ROWCOUNT',
1093
+ 'ROWS'
1094
+ ]),
1095
+ columnSpacingMil: PcbModelParser.#firstNumberField(fields, [
1096
+ 'COLUMNSPACING',
1097
+ 'COLSPACING',
1098
+ 'XSPACING'
1099
+ ]),
1100
+ rowSpacingMil: PcbModelParser.#firstNumberField(fields, [
1101
+ 'ROWSPACING',
1102
+ 'YSPACING'
1103
+ ])
1104
+ })
1105
+ const flags = PcbModelParser.#stripEmptyObject({
1106
+ keepIn: PcbModelParser.#optionalBooleanAny(fields, [
1107
+ 'KEEPIN',
1108
+ 'KEEPINSIDE'
1109
+ ]),
1110
+ keepout: PcbModelParser.#optionalBooleanAny(fields, ['KEEPOUT']),
1111
+ electrical: PcbModelParser.#optionalBooleanAny(fields, [
1112
+ 'ELECTRICAL'
1113
+ ]),
1114
+ preRoute: PcbModelParser.#optionalBooleanAny(fields, ['PREROUTE']),
1115
+ teardrops: PcbModelParser.#optionalBooleanAny(fields, [
1116
+ 'TEARDROPS'
1117
+ ]),
1118
+ polygons: PcbModelParser.#optionalBooleanAny(fields, ['POLYGONS']),
1119
+ transmitBoardShape: PcbModelParser.#optionalBooleanAny(fields, [
1120
+ 'TRANSMITBOARDSHAPE'
1121
+ ])
1122
+ })
1123
+
1124
+ return PcbModelParser.#stripEmptyObject({
1125
+ embeddedBoardIndex,
1126
+ documentPath,
1127
+ fileName: PcbModelParser.#basenameFromAltiumPath(documentPath),
1128
+ layer: PcbModelParser.#firstField(fields, ['LAYER', 'LAYERNAME']),
1129
+ rotation: PcbModelParser.#firstNumberField(fields, ['ROTATION']),
1130
+ mirrored: PcbModelParser.#optionalBooleanAny(fields, [
1131
+ 'MIRRORFLAG',
1132
+ 'MIRRORED'
1133
+ ]),
1134
+ originMode: PcbModelParser.#firstNumberField(fields, [
1135
+ 'ORIGINMODE'
1136
+ ]),
1137
+ scale: PcbModelParser.#firstNumberField(fields, ['SCALE']),
1138
+ ...(Object.keys(array).length ? { array } : {}),
1139
+ uniqueId: PcbModelParser.#firstField(fields, ['UNIQUEID', 'UID']),
1140
+ ...(Object.keys(flags).length ? { flags } : {})
1141
+ })
1142
+ }
1143
+
1144
+ /**
1145
+ * Parses PCB placement room records.
1146
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records Source records.
1147
+ * @returns {object[]}
1148
+ */
1149
+ static #parseRoomRecords(records) {
1150
+ return (records || [])
1151
+ .filter((record) => record.sourceStream === 'Rooms6/Data')
1152
+ .map((record, index) =>
1153
+ PcbModelParser.#normalizeRoomRecord(record.fields, index)
1154
+ )
1155
+ .filter(
1156
+ (room) => room.name || room.uniqueId || room.members?.length
1157
+ )
1158
+ }
1159
+
1160
+ /**
1161
+ * Normalizes one placement room record.
1162
+ * @param {Record<string, string | string[]>} fields Native fields.
1163
+ * @param {number} roomIndex Fallback index.
1164
+ * @returns {object}
1165
+ */
1166
+ static #normalizeRoomRecord(fields, roomIndex) {
1167
+ const bounds = PcbModelParser.#stripEmptyObject({
1168
+ x1: PcbModelParser.#firstNumberField(fields, ['X1', 'LEFT']),
1169
+ y1: PcbModelParser.#firstNumberField(fields, ['Y1', 'TOP']),
1170
+ x2: PcbModelParser.#firstNumberField(fields, ['X2', 'RIGHT']),
1171
+ y2: PcbModelParser.#firstNumberField(fields, ['Y2', 'BOTTOM'])
1172
+ })
1173
+
1174
+ return PcbModelParser.#stripEmptyObject({
1175
+ roomIndex,
1176
+ name: PcbModelParser.#firstField(fields, ['NAME', 'ROOMNAME']),
1177
+ uniqueId: PcbModelParser.#firstField(fields, ['UNIQUEID', 'UID']),
1178
+ members: PcbModelParser.#parseClassMembers(fields),
1179
+ ...(Object.keys(bounds).length ? { bounds } : {})
1180
+ })
1181
+ }
1182
+
998
1183
  /**
999
1184
  * Parses native DifferentialPairs6/Data records in stream order.
1000
1185
  * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
@@ -1150,6 +1335,23 @@ export class PcbModelParser {
1150
1335
  return raw ? parseBoolean(raw) : null
1151
1336
  }
1152
1337
 
1338
+ /**
1339
+ * Reads the first optional boolean from a list of field aliases.
1340
+ * @param {Record<string, string | string[]>} fields Native fields.
1341
+ * @param {string[]} keys Candidate field names.
1342
+ * @returns {boolean | null}
1343
+ */
1344
+ static #optionalBooleanAny(fields, keys) {
1345
+ for (const key of keys || []) {
1346
+ const raw = getField(fields, key)
1347
+ if (raw) {
1348
+ return parseBoolean(raw)
1349
+ }
1350
+ }
1351
+
1352
+ return null
1353
+ }
1354
+
1153
1355
  /**
1154
1356
  * Builds a case-insensitive lookup key for class and pair names.
1155
1357
  * @param {string | undefined} value Raw lookup value.
@@ -1435,6 +1637,23 @@ export class PcbModelParser {
1435
1637
  return null
1436
1638
  }
1437
1639
 
1640
+ /**
1641
+ * Returns the first numeric field value from a list of field aliases.
1642
+ * @param {Record<string, string | string[]>} fields Source fields.
1643
+ * @param {string[]} keys Candidate keys.
1644
+ * @returns {number | null}
1645
+ */
1646
+ static #firstNumberField(fields, keys) {
1647
+ for (const key of keys || []) {
1648
+ const parsed = parseNumericField(fields, key)
1649
+ if (Number.isFinite(parsed)) {
1650
+ return parsed
1651
+ }
1652
+ }
1653
+
1654
+ return null
1655
+ }
1656
+
1438
1657
  /**
1439
1658
  * Returns the first non-empty printable field value.
1440
1659
  * @param {Record<string, string | string[]>} fields Source fields.