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.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +132 -1
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +692 -2
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +7 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +16 -5
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +246 -7
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +182 -18
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +3 -0
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +45 -0
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +61 -2
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +35 -2
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +164 -0
- package/src/parser.mjs +15 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- 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 =
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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),
|