altium-toolkit 1.0.9 → 1.1.0

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 (79) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
  11. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
  12. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  13. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  14. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  15. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  16. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  17. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  18. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  19. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
  20. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  21. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  22. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
  23. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  24. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  25. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  26. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  27. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  28. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  29. package/package.json +1 -1
  30. package/src/core/altium/AltiumParser.mjs +12 -2
  31. package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
  32. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  33. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  34. package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
  35. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  36. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  37. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  38. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  39. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  40. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  41. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  42. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  43. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  44. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  45. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  46. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  47. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  48. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  49. package/src/core/altium/PcbModelParser.mjs +211 -22
  50. package/src/core/altium/PcbPadStackParser.mjs +171 -2
  51. package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
  52. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  53. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  54. package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
  55. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  56. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  57. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  58. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  59. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  60. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  61. package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
  62. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  63. package/src/core/altium/PrjPcbModelParser.mjs +69 -2
  64. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  65. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  66. package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
  67. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  68. package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
  69. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  70. package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
  71. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
  72. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  73. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  74. package/src/parser.mjs +21 -0
  75. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  76. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  77. package/src/ui/PcbSvgRenderer.mjs +65 -0
  78. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -0,0 +1,242 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds parity reports for advanced PcbLib footprint fields.
7
+ */
8
+ export class PcbLibParityReportBuilder {
9
+ static SCHEMA = 'altium-toolkit.pcblib.parity.a1'
10
+
11
+ /**
12
+ * Builds an advanced-field parity report.
13
+ * @param {{ footprints?: object[] }} pcbLibrary Parsed PCB library model.
14
+ * @returns {object}
15
+ */
16
+ static build(pcbLibrary = {}) {
17
+ const footprints = (pcbLibrary.footprints || []).map((footprint) =>
18
+ PcbLibParityReportBuilder.#footprintRow(footprint)
19
+ )
20
+ const summary = PcbLibParityReportBuilder.#summary(footprints)
21
+
22
+ return {
23
+ schema: PcbLibParityReportBuilder.SCHEMA,
24
+ summary,
25
+ footprints
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Builds one footprint parity row.
31
+ * @param {object} footprint Footprint record.
32
+ * @returns {object}
33
+ */
34
+ static #footprintRow(footprint) {
35
+ const advancedFields = {
36
+ localStackPads: (footprint.pads || []).filter(
37
+ (pad) => pad.localStack
38
+ ).length,
39
+ customPadShapes: footprint.customPadShapes?.entries?.length || 0,
40
+ maskPastePrimitives: footprint.maskPaste?.primitives?.length || 0,
41
+ viaTenting: (footprint.vias || []).filter((via) =>
42
+ PcbLibParityReportBuilder.#hasViaTenting(via)
43
+ ).length,
44
+ barcodeTexts: (footprint.texts || []).filter((text) => text.barcode)
45
+ .length,
46
+ embeddedModels: (footprint.embeddedModels || []).length,
47
+ projectionDiagnostics: (footprint.componentBodies || []).filter(
48
+ (body) => body.projectionDiagnostics
49
+ ).length
50
+ }
51
+
52
+ return {
53
+ name: footprint.name || '',
54
+ advancedFields,
55
+ layers: PcbLibParityReportBuilder.#layers(footprint),
56
+ diagnostics: PcbLibParityReportBuilder.#diagnostics(advancedFields)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Builds top-level parity counters.
62
+ * @param {object[]} footprints Footprint parity rows.
63
+ * @returns {object}
64
+ */
65
+ static #summary(footprints) {
66
+ return {
67
+ footprintCount: footprints.length,
68
+ footprintWithAdvancedFieldsCount: footprints.filter((footprint) =>
69
+ Object.values(footprint.advancedFields).some(
70
+ (value) => Number(value) > 0
71
+ )
72
+ ).length,
73
+ localStackPadCount: PcbLibParityReportBuilder.#sum(
74
+ footprints,
75
+ 'localStackPads'
76
+ ),
77
+ customPadFootprintCount: footprints.filter(
78
+ (footprint) => footprint.advancedFields.customPadShapes > 0
79
+ ).length,
80
+ maskPastePrimitiveCount: PcbLibParityReportBuilder.#sum(
81
+ footprints,
82
+ 'maskPastePrimitives'
83
+ ),
84
+ viaTentingCount: PcbLibParityReportBuilder.#sum(
85
+ footprints,
86
+ 'viaTenting'
87
+ ),
88
+ barcodeTextCount: PcbLibParityReportBuilder.#sum(
89
+ footprints,
90
+ 'barcodeTexts'
91
+ ),
92
+ embeddedModelFootprintCount: footprints.filter(
93
+ (footprint) => footprint.advancedFields.embeddedModels > 0
94
+ ).length,
95
+ projectionDiagnosticCount: PcbLibParityReportBuilder.#sum(
96
+ footprints,
97
+ 'projectionDiagnostics'
98
+ )
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Sums one advanced-field counter.
104
+ * @param {object[]} footprints Footprint rows.
105
+ * @param {string} key Advanced-field key.
106
+ * @returns {number}
107
+ */
108
+ static #sum(footprints, key) {
109
+ return footprints.reduce(
110
+ (total, footprint) =>
111
+ total + Number(footprint.advancedFields?.[key] || 0),
112
+ 0
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Builds layer descriptors represented by the footprint.
118
+ * @param {object} footprint Footprint record.
119
+ * @returns {object[]}
120
+ */
121
+ static #layers(footprint) {
122
+ const layerMap = new Map()
123
+ for (const primitive of PcbLibParityReportBuilder.#primitives(
124
+ footprint
125
+ )) {
126
+ const layer = PcbLibParityReportBuilder.#layerDescriptor(primitive)
127
+ if (layer) layerMap.set(layer.layerKey, layer)
128
+ }
129
+ return [...layerMap.values()].sort((left, right) =>
130
+ left.layerKey.localeCompare(right.layerKey, undefined, {
131
+ numeric: true
132
+ })
133
+ )
134
+ }
135
+
136
+ /**
137
+ * Builds diagnostics for unsupported parity edge cases.
138
+ * @param {object} advancedFields Advanced-field counts.
139
+ * @returns {object[]}
140
+ */
141
+ static #diagnostics(advancedFields) {
142
+ return Object.values(advancedFields).some((value) => Number(value) > 0)
143
+ ? []
144
+ : [
145
+ {
146
+ code: 'pcblib.parity.no-advanced-fields',
147
+ severity: 'info',
148
+ message:
149
+ 'Footprint does not expose advanced PCB field families.'
150
+ }
151
+ ]
152
+ }
153
+
154
+ /**
155
+ * Returns true when a via preserves tenting metadata.
156
+ * @param {object} via Via primitive.
157
+ * @returns {boolean}
158
+ */
159
+ static #hasViaTenting(via) {
160
+ return [
161
+ via?.topTenting,
162
+ via?.bottomTenting,
163
+ via?.tentingTop,
164
+ via?.tentingBottom,
165
+ via?.solderMaskExpansionMode,
166
+ via?.solderMaskExpansion
167
+ ].some((value) => value !== undefined && value !== null && value !== '')
168
+ }
169
+
170
+ /**
171
+ * Returns all footprint primitives that can carry layer metadata.
172
+ * @param {object} footprint Footprint record.
173
+ * @returns {object[]}
174
+ */
175
+ static #primitives(footprint) {
176
+ return [
177
+ ...(footprint.pads || []),
178
+ ...(footprint.vias || []),
179
+ ...(footprint.tracks || []),
180
+ ...(footprint.arcs || []),
181
+ ...(footprint.fills || []),
182
+ ...(footprint.regions || []),
183
+ ...(footprint.shapeBasedRegions || []),
184
+ ...(footprint.texts || [])
185
+ ]
186
+ }
187
+
188
+ /**
189
+ * Builds a normalized layer descriptor.
190
+ * @param {object} primitive Primitive row.
191
+ * @returns {object | null}
192
+ */
193
+ static #layerDescriptor(primitive) {
194
+ const layerId = Number.isInteger(primitive?.layerId)
195
+ ? primitive.layerId
196
+ : null
197
+ const displayName = String(
198
+ primitive?.layerName || primitive?.layer || ''
199
+ ).trim()
200
+ if (layerId === null && !displayName) {
201
+ return null
202
+ }
203
+ const layerKey =
204
+ layerId === null
205
+ ? 'layer-' + PcbLibParityReportBuilder.#slug(displayName)
206
+ : 'L' + layerId
207
+
208
+ return PcbLibParityReportBuilder.#stripUndefined({
209
+ layerKey,
210
+ layerId: layerId === null ? undefined : layerId,
211
+ displayName: displayName || layerKey
212
+ })
213
+ }
214
+
215
+ /**
216
+ * Converts a value to a deterministic lowercase key segment.
217
+ * @param {unknown} value Source value.
218
+ * @returns {string}
219
+ */
220
+ static #slug(value) {
221
+ return (
222
+ String(value || '')
223
+ .trim()
224
+ .toLowerCase()
225
+ .replace(/[^a-z0-9]+/gu, '-')
226
+ .replace(/^-+|-+$/gu, '') || 'item'
227
+ )
228
+ }
229
+
230
+ /**
231
+ * Removes undefined fields.
232
+ * @param {Record<string, unknown>} value Candidate object.
233
+ * @returns {Record<string, unknown>}
234
+ */
235
+ static #stripUndefined(value) {
236
+ return Object.fromEntries(
237
+ Object.entries(value || {}).filter(
238
+ ([, entryValue]) => entryValue !== undefined
239
+ )
240
+ )
241
+ }
242
+ }
@@ -5,17 +5,25 @@
5
5
  import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
6
6
  import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
7
7
  import { PcbBoardRegionSemanticsParser } from './PcbBoardRegionSemanticsParser.mjs'
8
+ import { PcbBomProfileBuilder } from './PcbBomProfileBuilder.mjs'
8
9
  import { PcbComponentAnnotationNormalizer } from './PcbComponentAnnotationNormalizer.mjs'
9
10
  import { PcbComponentBodyPlacementNormalizer } from './PcbComponentBodyPlacementNormalizer.mjs'
11
+ import { PcbComponentKindPolicy } from './PcbComponentKindPolicy.mjs'
10
12
  import { PcbComponentPrimitiveIndexer } from './PcbComponentPrimitiveIndexer.mjs'
11
13
  import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
12
14
  import { PcbDimensionParser } from './PcbDimensionParser.mjs'
15
+ import { PcbLayerStackReadModelBuilder } from './PcbLayerStackReadModelBuilder.mjs'
13
16
  import { PcbMechanicalLayerPairParser } from './PcbMechanicalLayerPairParser.mjs'
14
17
  import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
15
18
  import { PcbMaskPasteResolver } from './PcbMaskPasteResolver.mjs'
16
19
  import { PcbOutlineRecovery } from './PcbOutlineRecovery.mjs'
17
20
  import { PcbOwnershipGraphBuilder } from './PcbOwnershipGraphBuilder.mjs'
21
+ import { PcbPlacedFootprintManifestBuilder } from './PcbPlacedFootprintManifestBuilder.mjs'
18
22
  import { PcbPickPlacePositionResolver } from './PcbPickPlacePositionResolver.mjs'
23
+ import { PcbPolygonRecordParser } from './PcbPolygonRecordParser.mjs'
24
+ import { PcbReviewMetadataBuilder } from './PcbReviewMetadataBuilder.mjs'
25
+ import { PcbRigidFlexTopologyBuilder } from './PcbRigidFlexTopologyBuilder.mjs'
26
+ import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
19
27
  import { PcbRuleParser } from './PcbRuleParser.mjs'
20
28
  import { PcbSpecialStringResolver } from './PcbSpecialStringResolver.mjs'
21
29
  import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
@@ -84,11 +92,6 @@ export class PcbModelParser {
84
92
  PcbModelParser.#publicComponentRecord(component)
85
93
  )
86
94
  )
87
- const polygonRecords = records.filter(
88
- (record) =>
89
- record.sourceStream === 'Polygons6/Data' &&
90
- getField(record.fields, 'KIND0')
91
- )
92
95
  const fallbackBoardOutline = AltiumLayoutParser.parseBoardOutline(
93
96
  boardRecord?.fields || {}
94
97
  )
@@ -123,13 +126,7 @@ export class PcbModelParser {
123
126
  'pcb-document'
124
127
  )
125
128
  const dimensions = PcbDimensionParser.parse(records)
126
- const polygons = polygonRecords
127
- .map((record) => ({
128
- layer: getField(record.fields, 'LAYER') || 'UNKNOWN',
129
- segments: AltiumLayoutParser.parseBoardOutline(record.fields)
130
- .segments
131
- }))
132
- .filter((polygon) => polygon.segments.length > 0)
129
+ const polygons = PcbPolygonRecordParser.parse(records)
133
130
  const tracks = PcbModelParser.#annotatePrimitiveNetNames(
134
131
  pcbExtraction?.binaryPrimitives?.tracks || [],
135
132
  netNameByIndex
@@ -258,6 +255,17 @@ export class PcbModelParser {
258
255
  PcbBoardRegionSemanticsParser.summarizeBoardRegions(
259
256
  normalizedPcb.boardRegions
260
257
  )
258
+ const layerStackReadModel = PcbLayerStackReadModelBuilder.build({
259
+ fileName,
260
+ boardRecords,
261
+ streamNames: pcbExtraction?.streamNames || [],
262
+ layers,
263
+ primitiveLayers,
264
+ layerSubstacks,
265
+ boardRegions: normalizedPcb.boardRegions
266
+ })
267
+ const rigidFlexTopology =
268
+ PcbRigidFlexTopologyBuilder.build(layerStackReadModel)
261
269
  const componentBodies =
262
270
  PcbComponentBodyPlacementNormalizer.normalizeComponentBodies(
263
271
  extractedComponentBodies,
@@ -281,6 +289,38 @@ export class PcbModelParser {
281
289
  componentPrimitiveGroups,
282
290
  { sourceComponents: componentRecords }
283
291
  )
292
+ const routeAnalysis = PcbRouteAnalysisBuilder.build({
293
+ ...normalizedPcb,
294
+ layers,
295
+ primitiveLayers,
296
+ nets,
297
+ classes,
298
+ differentialPairs: differentialPairData.differentialPairs,
299
+ differentialPairClasses:
300
+ differentialPairData.differentialPairClasses
301
+ })
302
+ const reviewMetadata = PcbReviewMetadataBuilder.build({
303
+ routeAnalysis,
304
+ embeddedModels: extractedEmbeddedModels,
305
+ componentBodies,
306
+ layers,
307
+ primitiveLayers,
308
+ polygons: normalizedPcb.polygons,
309
+ tracks: normalizedPcb.tracks,
310
+ arcs: normalizedPcb.arcs,
311
+ fills: normalizedPcb.fills,
312
+ vias: normalizedPcb.vias,
313
+ pads: normalizedPcb.pads,
314
+ regions: normalizedPcb.regions,
315
+ shapeBasedRegions: normalizedPcb.shapeBasedRegions
316
+ })
317
+ const footprintExtractionManifest =
318
+ PcbPlacedFootprintManifestBuilder.build({
319
+ fileName,
320
+ components: normalizedPcb.components,
321
+ componentPrimitiveGroups,
322
+ embeddedModels: extractedEmbeddedModels
323
+ })
284
324
  const statistics = PcbStatisticsBuilder.build({
285
325
  ...normalizedPcb,
286
326
  layers,
@@ -294,13 +334,21 @@ export class PcbModelParser {
294
334
  defaults
295
335
  })
296
336
  const bom = PcbModelParser.#groupBomRows(
297
- componentRecords.map((component) => ({
298
- designator: component.designator,
299
- pattern: component.pattern,
300
- source: component.source,
301
- value: component.description || component.pattern
302
- }))
337
+ componentRecords
338
+ .filter(
339
+ (component) =>
340
+ component.componentKind?.includeInBom !== false
341
+ )
342
+ .map((component) => ({
343
+ designator: component.designator,
344
+ pattern: component.pattern,
345
+ source: component.source,
346
+ value: component.description || component.pattern
347
+ }))
303
348
  )
349
+ const bomProfile = PcbBomProfileBuilder.build(componentRecords, {
350
+ source: 'pcb-document'
351
+ })
304
352
 
305
353
  const diagnostics = [
306
354
  {
@@ -358,6 +406,22 @@ export class PcbModelParser {
358
406
  })
359
407
  }
360
408
 
409
+ for (const issue of layerStackReadModel?.diagnostics || []) {
410
+ diagnostics.push({
411
+ severity: issue.severity || 'warning',
412
+ code: issue.code,
413
+ message: issue.message
414
+ })
415
+ }
416
+
417
+ for (const issue of rigidFlexTopology?.diagnostics || []) {
418
+ diagnostics.push({
419
+ severity: issue.severity || 'warning',
420
+ code: issue.code,
421
+ message: issue.message
422
+ })
423
+ }
424
+
361
425
  if (pcbExtraction) {
362
426
  diagnostics.push({
363
427
  severity: 'info',
@@ -544,6 +608,13 @@ export class PcbModelParser {
544
608
  differentialPairClassCount:
545
609
  differentialPairData.differentialPairClasses.length,
546
610
  ruleCount: rules.length,
611
+ routeReviewGroupCount:
612
+ reviewMetadata.summary.routeGroupCount || 0,
613
+ boardAssemblyViewCount:
614
+ reviewMetadata.summary.boardAssemblyViewCount || 0,
615
+ extractableFootprintCount:
616
+ footprintExtractionManifest.summary
617
+ .extractableFootprintCount || 0,
547
618
  dimensionCount: dimensions.length,
548
619
  mechanicalLayerPairCount: mechanicalLayerPairs.length,
549
620
  polygonCount: polygons.length,
@@ -556,9 +627,23 @@ export class PcbModelParser {
556
627
  customPadShapeCount: customPadShapes.entries?.length || 0,
557
628
  userUnionCount: unions.userUnions?.length || 0,
558
629
  smartUnionCount: unions.smartUnions?.length || 0,
630
+ routedNetCount: routeAnalysis.summary.routedNetCount,
631
+ routedLengthMil: routeAnalysis.summary.totalLengthMil,
559
632
  boardRegionCount: boardRegionSummary.boardRegionCount,
560
633
  flexRegionCount: boardRegionSummary.flexRegionCount,
561
634
  bendingLineCount: boardRegionSummary.bendingLineCount,
635
+ layerStackSubstackCount:
636
+ layerStackReadModel?.summary.substackCount || 0,
637
+ layerStackBranchCount:
638
+ layerStackReadModel?.summary.branchCount || 0,
639
+ impedanceProfileCount:
640
+ layerStackReadModel?.summary.impedanceProfileCount || 0,
641
+ backdrillSpanCount:
642
+ layerStackReadModel?.summary.backdrillSpanCount || 0,
643
+ cavityRegionCount:
644
+ layerStackReadModel?.summary.cavityRegionCount || 0,
645
+ stiffenerLayerCount:
646
+ layerStackReadModel?.summary.stiffenerLayerCount || 0,
562
647
  embeddedModelIssueCount:
563
648
  embeddedModelIntegrity.issues?.length || 0,
564
649
  embeddedFontCount: extractedEmbeddedFonts.length,
@@ -572,6 +657,8 @@ export class PcbModelParser {
572
657
  boardOutline: normalizedPcb.boardOutline,
573
658
  layers,
574
659
  layerSubstacks,
660
+ ...(layerStackReadModel ? { layerStackReadModel } : {}),
661
+ ...(rigidFlexTopology ? { rigidFlexTopology } : {}),
575
662
  mechanicalLayerPairs,
576
663
  layerFlipMetadata,
577
664
  boardRegionContexts,
@@ -585,9 +672,13 @@ export class PcbModelParser {
585
672
  rules,
586
673
  ...(defaults ? { defaults } : {}),
587
674
  maskPaste,
675
+ bomProfile,
588
676
  dimensions,
589
677
  components: normalizedPcb.components,
590
678
  pickPlace: pnp,
679
+ routeAnalysis,
680
+ reviewMetadata,
681
+ footprintExtractionManifest,
591
682
  polygons: normalizedPcb.polygons,
592
683
  fills: normalizedPcb.fills,
593
684
  tracks: normalizedPcb.tracks,
@@ -651,6 +742,12 @@ export class PcbModelParser {
651
742
  const provenance = PcbModelParser.#parseComponentProvenance(
652
743
  record.fields
653
744
  )
745
+ const componentKind = PcbComponentKindPolicy.parse(
746
+ record.fields
747
+ )
748
+ const parameters = PcbModelParser.#parseComponentParameters(
749
+ record.fields
750
+ )
654
751
 
655
752
  return {
656
753
  componentIndex: index,
@@ -669,6 +766,8 @@ export class PcbModelParser {
669
766
  getField(record.fields, 'SOURCEFOOTPRINTLIBRARY'),
670
767
  description: getField(record.fields, 'SOURCEDESCRIPTION'),
671
768
  height: parseNumericField(record.fields, 'HEIGHT'),
769
+ ...(Object.keys(parameters).length ? { parameters } : {}),
770
+ ...(componentKind ? { componentKind } : {}),
672
771
  ...(Object.keys(provenance).length ? { provenance } : {}),
673
772
  nameOn: parseBoolean(record.fields.NAMEON),
674
773
  commentOn: parseBoolean(record.fields.COMMENTON)
@@ -743,6 +842,70 @@ export class PcbModelParser {
743
842
  return nonRedundantKeys.length ? provenance : {}
744
843
  }
745
844
 
845
+ /**
846
+ * Parses component parameter name/value rows from printable component data.
847
+ * @param {Record<string, string | string[]>} fields Component fields.
848
+ * @returns {Record<string, string>}
849
+ */
850
+ static #parseComponentParameters(fields) {
851
+ const parameters = {}
852
+ const indexes = PcbModelParser.#componentParameterIndexes(fields)
853
+
854
+ for (const index of indexes) {
855
+ const name = PcbModelParser.#firstField(fields, [
856
+ 'PARAMETER' + index + 'NAME',
857
+ 'PARAMETER' + index + '_NAME',
858
+ 'PARAMETERNAME' + index,
859
+ 'PARAMETER_NAME' + index
860
+ ])
861
+ const value = PcbModelParser.#firstField(fields, [
862
+ 'PARAMETER' + index + 'VALUE',
863
+ 'PARAMETER' + index + '_VALUE',
864
+ 'PARAMETERVALUE' + index,
865
+ 'PARAMETER_VALUE' + index,
866
+ 'PARAMETER' + index + 'TEXT',
867
+ 'PARAMETERTEXT' + index
868
+ ])
869
+ if (!name) continue
870
+ parameters[name] = value
871
+ }
872
+
873
+ return parameters
874
+ }
875
+
876
+ /**
877
+ * Collects component parameter indexes from count and field names.
878
+ * @param {Record<string, string | string[]>} fields Component fields.
879
+ * @returns {number[]}
880
+ */
881
+ static #componentParameterIndexes(fields) {
882
+ const indexes = new Set()
883
+ const count =
884
+ parseNumericField(fields, 'PARAMETERCOUNT') ??
885
+ parseNumericField(fields, 'PARAMETERSCOUNT')
886
+
887
+ if (Number.isInteger(count) && count > 0) {
888
+ for (let index = 0; index < count; index += 1) {
889
+ indexes.add(index)
890
+ }
891
+ }
892
+
893
+ for (const key of Object.keys(fields || {})) {
894
+ const match = /^PARAMETER_?(\d+)_?(NAME|VALUE|TEXT)$/iu.exec(key)
895
+ if (match) {
896
+ indexes.add(Number.parseInt(match[1], 10))
897
+ }
898
+ const alternateMatch = /^PARAMETER(NAME|VALUE|TEXT)(\d+)$/iu.exec(
899
+ key
900
+ )
901
+ if (alternateMatch) {
902
+ indexes.add(Number.parseInt(alternateMatch[2], 10))
903
+ }
904
+ }
905
+
906
+ return [...indexes].sort((left, right) => left - right)
907
+ }
908
+
746
909
  /**
747
910
  * Normalizes native Nets6/Data records in stream order.
748
911
  * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
@@ -1193,25 +1356,36 @@ export class PcbModelParser {
1193
1356
  key === 'MEMBERCOUNT' ||
1194
1357
  key === 'ENABLED' ||
1195
1358
  key === 'UNIQUEID' ||
1196
- /^M\d+$/.test(key)
1359
+ /^(?:M|MEMBER)\d+$/.test(key)
1197
1360
  )
1198
1361
  }
1199
1362
 
1200
1363
  /**
1201
- * Extracts ordered class members from M0, M1, ... fields.
1364
+ * Extracts ordered class members from M0/MEMBER0-style fields.
1202
1365
  * @param {Record<string, string | string[]>} fields
1203
1366
  * @returns {string[]}
1204
1367
  */
1205
1368
  static #parseClassMembers(fields) {
1206
1369
  return Object.keys(fields || {})
1207
- .filter((key) => /^M\d+$/.test(key))
1370
+ .filter((key) => /^(?:M|MEMBER)\d+$/.test(key))
1208
1371
  .sort(
1209
- (left, right) => Number(left.slice(1)) - Number(right.slice(1))
1372
+ (left, right) =>
1373
+ PcbModelParser.#classMemberIndex(left) -
1374
+ PcbModelParser.#classMemberIndex(right)
1210
1375
  )
1211
1376
  .map((key) => getField(fields, key))
1212
1377
  .filter(Boolean)
1213
1378
  }
1214
1379
 
1380
+ /**
1381
+ * Extracts the numeric index from a class member field name.
1382
+ * @param {string} key Field key.
1383
+ * @returns {number}
1384
+ */
1385
+ static #classMemberIndex(key) {
1386
+ return Number(String(key).replace(/^(?:M|MEMBER)/u, ''))
1387
+ }
1388
+
1215
1389
  /**
1216
1390
  * Returns a stable display name for one native PCB class kind.
1217
1391
  * @param {number} kind
@@ -1261,6 +1435,21 @@ export class PcbModelParser {
1261
1435
  return null
1262
1436
  }
1263
1437
 
1438
+ /**
1439
+ * Returns the first non-empty printable field value.
1440
+ * @param {Record<string, string | string[]>} fields Source fields.
1441
+ * @param {string[]} keys Candidate keys.
1442
+ * @returns {string}
1443
+ */
1444
+ static #firstField(fields, keys) {
1445
+ for (const key of keys) {
1446
+ const value = getField(fields, key)
1447
+ if (value) return value
1448
+ }
1449
+
1450
+ return ''
1451
+ }
1452
+
1264
1453
  /**
1265
1454
  * Groups component placements into BOM rows.
1266
1455
  * @param {{ designator: string, pattern: string, source: string, value: string }[]} componentRecords