altium-toolkit 1.0.10 → 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 (63) 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 +4 -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 +132 -1
  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/normalized_model_a1.schema.json +692 -2
  11. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  12. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  13. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  14. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  15. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  16. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  17. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  18. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  19. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  20. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  21. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  22. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  23. package/package.json +1 -1
  24. package/src/core/altium/AltiumParser.mjs +7 -2
  25. package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
  26. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  27. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  28. package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
  29. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  30. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  31. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  32. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  33. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  34. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  35. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  36. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  37. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  38. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  39. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  40. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  41. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  42. package/src/core/altium/PcbModelParser.mjs +182 -18
  43. package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
  44. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  45. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  46. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  47. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  48. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  49. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  50. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  51. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  52. package/src/core/altium/PrjPcbModelParser.mjs +45 -0
  53. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  54. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  55. package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
  56. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  57. package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
  58. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
  59. package/src/parser.mjs +15 -0
  60. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  61. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  62. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  63. 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,24 @@
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'
19
26
  import { PcbRouteAnalysisBuilder } from './PcbRouteAnalysisBuilder.mjs'
20
27
  import { PcbRuleParser } from './PcbRuleParser.mjs'
21
28
  import { PcbSpecialStringResolver } from './PcbSpecialStringResolver.mjs'
@@ -85,11 +92,6 @@ export class PcbModelParser {
85
92
  PcbModelParser.#publicComponentRecord(component)
86
93
  )
87
94
  )
88
- const polygonRecords = records.filter(
89
- (record) =>
90
- record.sourceStream === 'Polygons6/Data' &&
91
- getField(record.fields, 'KIND0')
92
- )
93
95
  const fallbackBoardOutline = AltiumLayoutParser.parseBoardOutline(
94
96
  boardRecord?.fields || {}
95
97
  )
@@ -124,13 +126,7 @@ export class PcbModelParser {
124
126
  'pcb-document'
125
127
  )
126
128
  const dimensions = PcbDimensionParser.parse(records)
127
- const polygons = polygonRecords
128
- .map((record) => ({
129
- layer: getField(record.fields, 'LAYER') || 'UNKNOWN',
130
- segments: AltiumLayoutParser.parseBoardOutline(record.fields)
131
- .segments
132
- }))
133
- .filter((polygon) => polygon.segments.length > 0)
129
+ const polygons = PcbPolygonRecordParser.parse(records)
134
130
  const tracks = PcbModelParser.#annotatePrimitiveNetNames(
135
131
  pcbExtraction?.binaryPrimitives?.tracks || [],
136
132
  netNameByIndex
@@ -259,6 +255,17 @@ export class PcbModelParser {
259
255
  PcbBoardRegionSemanticsParser.summarizeBoardRegions(
260
256
  normalizedPcb.boardRegions
261
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)
262
269
  const componentBodies =
263
270
  PcbComponentBodyPlacementNormalizer.normalizeComponentBodies(
264
271
  extractedComponentBodies,
@@ -292,6 +299,28 @@ export class PcbModelParser {
292
299
  differentialPairClasses:
293
300
  differentialPairData.differentialPairClasses
294
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
+ })
295
324
  const statistics = PcbStatisticsBuilder.build({
296
325
  ...normalizedPcb,
297
326
  layers,
@@ -305,13 +334,21 @@ export class PcbModelParser {
305
334
  defaults
306
335
  })
307
336
  const bom = PcbModelParser.#groupBomRows(
308
- componentRecords.map((component) => ({
309
- designator: component.designator,
310
- pattern: component.pattern,
311
- source: component.source,
312
- value: component.description || component.pattern
313
- }))
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
+ }))
314
348
  )
349
+ const bomProfile = PcbBomProfileBuilder.build(componentRecords, {
350
+ source: 'pcb-document'
351
+ })
315
352
 
316
353
  const diagnostics = [
317
354
  {
@@ -369,6 +406,22 @@ export class PcbModelParser {
369
406
  })
370
407
  }
371
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
+
372
425
  if (pcbExtraction) {
373
426
  diagnostics.push({
374
427
  severity: 'info',
@@ -555,6 +608,13 @@ export class PcbModelParser {
555
608
  differentialPairClassCount:
556
609
  differentialPairData.differentialPairClasses.length,
557
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,
558
618
  dimensionCount: dimensions.length,
559
619
  mechanicalLayerPairCount: mechanicalLayerPairs.length,
560
620
  polygonCount: polygons.length,
@@ -572,6 +632,18 @@ export class PcbModelParser {
572
632
  boardRegionCount: boardRegionSummary.boardRegionCount,
573
633
  flexRegionCount: boardRegionSummary.flexRegionCount,
574
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,
575
647
  embeddedModelIssueCount:
576
648
  embeddedModelIntegrity.issues?.length || 0,
577
649
  embeddedFontCount: extractedEmbeddedFonts.length,
@@ -585,6 +657,8 @@ export class PcbModelParser {
585
657
  boardOutline: normalizedPcb.boardOutline,
586
658
  layers,
587
659
  layerSubstacks,
660
+ ...(layerStackReadModel ? { layerStackReadModel } : {}),
661
+ ...(rigidFlexTopology ? { rigidFlexTopology } : {}),
588
662
  mechanicalLayerPairs,
589
663
  layerFlipMetadata,
590
664
  boardRegionContexts,
@@ -598,10 +672,13 @@ export class PcbModelParser {
598
672
  rules,
599
673
  ...(defaults ? { defaults } : {}),
600
674
  maskPaste,
675
+ bomProfile,
601
676
  dimensions,
602
677
  components: normalizedPcb.components,
603
678
  pickPlace: pnp,
604
679
  routeAnalysis,
680
+ reviewMetadata,
681
+ footprintExtractionManifest,
605
682
  polygons: normalizedPcb.polygons,
606
683
  fills: normalizedPcb.fills,
607
684
  tracks: normalizedPcb.tracks,
@@ -665,6 +742,12 @@ export class PcbModelParser {
665
742
  const provenance = PcbModelParser.#parseComponentProvenance(
666
743
  record.fields
667
744
  )
745
+ const componentKind = PcbComponentKindPolicy.parse(
746
+ record.fields
747
+ )
748
+ const parameters = PcbModelParser.#parseComponentParameters(
749
+ record.fields
750
+ )
668
751
 
669
752
  return {
670
753
  componentIndex: index,
@@ -683,6 +766,8 @@ export class PcbModelParser {
683
766
  getField(record.fields, 'SOURCEFOOTPRINTLIBRARY'),
684
767
  description: getField(record.fields, 'SOURCEDESCRIPTION'),
685
768
  height: parseNumericField(record.fields, 'HEIGHT'),
769
+ ...(Object.keys(parameters).length ? { parameters } : {}),
770
+ ...(componentKind ? { componentKind } : {}),
686
771
  ...(Object.keys(provenance).length ? { provenance } : {}),
687
772
  nameOn: parseBoolean(record.fields.NAMEON),
688
773
  commentOn: parseBoolean(record.fields.COMMENTON)
@@ -757,6 +842,70 @@ export class PcbModelParser {
757
842
  return nonRedundantKeys.length ? provenance : {}
758
843
  }
759
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
+
760
909
  /**
761
910
  * Normalizes native Nets6/Data records in stream order.
762
911
  * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
@@ -1286,6 +1435,21 @@ export class PcbModelParser {
1286
1435
  return null
1287
1436
  }
1288
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
+
1289
1453
  /**
1290
1454
  * Groups component placements into BOM rows.
1291
1455
  * @param {{ designator: string, pattern: string, source: string, value: string }[]} componentRecords
@@ -144,6 +144,9 @@ export class PcbPickPlacePositionResolver {
144
144
  designator: component.designator || '',
145
145
  pattern: component.pattern || '',
146
146
  layer: component.layer || '',
147
+ ...(component.componentKind
148
+ ? { componentKind: component.componentKind }
149
+ : {}),
147
150
  rotation: PcbPickPlacePositionResolver.#roundCoordinate(rotation),
148
151
  x: PcbPickPlacePositionResolver.#roundCoordinate(position.x),
149
152
  y: PcbPickPlacePositionResolver.#roundCoordinate(position.y),