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.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -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 +166 -0
- 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/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- 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/pcb_svg_semantics_a1.schema.json +27 -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_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -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/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- 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/ParserCompatibilityFuzzer.mjs +192 -0
- 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 +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- 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/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- 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,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 =
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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) =>
|
|
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
|