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,906 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
import { PcbLayerStackFidelityReportBuilder } from './PcbLayerStackFidelityReportBuilder.mjs'
|
|
7
|
+
import { PcbLayerStackSourceMetadataParser } from './PcbLayerStackSourceMetadataParser.mjs'
|
|
8
|
+
|
|
9
|
+
const { parseNumericField } = ParserUtils
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a source-aware PCB layer-stack read model from decoded board records.
|
|
13
|
+
*/
|
|
14
|
+
export class PcbLayerStackReadModelBuilder {
|
|
15
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.layer-stack.a1'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds the layer-stack sidecar.
|
|
19
|
+
* @param {{ fileName: string, boardRecords: { fields: Record<string, string | string[]>, sourceStream?: string }[], streamNames?: string[], layers: object[], primitiveLayers: object[], layerSubstacks: object[], boardRegions: object[] }} input Source model context.
|
|
20
|
+
* @returns {object | undefined}
|
|
21
|
+
*/
|
|
22
|
+
static build(input) {
|
|
23
|
+
const fields = PcbLayerStackReadModelBuilder.#mergeFields(
|
|
24
|
+
input.boardRecords || []
|
|
25
|
+
)
|
|
26
|
+
const layers = PcbLayerStackReadModelBuilder.#layers(
|
|
27
|
+
input.layers || [],
|
|
28
|
+
input.primitiveLayers || [],
|
|
29
|
+
fields
|
|
30
|
+
)
|
|
31
|
+
const layerById = new Map(
|
|
32
|
+
layers
|
|
33
|
+
.filter((layer) => Number.isFinite(layer.layerId))
|
|
34
|
+
.map((layer) => [layer.layerId, layer])
|
|
35
|
+
)
|
|
36
|
+
const substacks = PcbLayerStackReadModelBuilder.#substacks(
|
|
37
|
+
input.layerSubstacks || [],
|
|
38
|
+
fields,
|
|
39
|
+
layerById,
|
|
40
|
+
input.boardRegions || []
|
|
41
|
+
)
|
|
42
|
+
const branches = PcbLayerStackReadModelBuilder.#branches(fields)
|
|
43
|
+
const topLevelBendLines =
|
|
44
|
+
PcbLayerStackSourceMetadataParser.topLevelBendLines(fields)
|
|
45
|
+
const cavityReport = PcbLayerStackSourceMetadataParser.cavityReport(
|
|
46
|
+
layers,
|
|
47
|
+
input.boardRegions || []
|
|
48
|
+
)
|
|
49
|
+
const impedanceProfiles =
|
|
50
|
+
PcbLayerStackReadModelBuilder.#impedanceProfiles(fields)
|
|
51
|
+
const transmissionLines =
|
|
52
|
+
PcbLayerStackReadModelBuilder.#transmissionLines(fields, layerById)
|
|
53
|
+
const viaSpans = PcbLayerStackReadModelBuilder.#layerSpans(
|
|
54
|
+
fields,
|
|
55
|
+
layerById,
|
|
56
|
+
'via'
|
|
57
|
+
)
|
|
58
|
+
const backdrillSpans = PcbLayerStackReadModelBuilder.#layerSpans(
|
|
59
|
+
fields,
|
|
60
|
+
layerById,
|
|
61
|
+
'backdrill'
|
|
62
|
+
)
|
|
63
|
+
const diagnostics = PcbLayerStackReadModelBuilder.#diagnostics({
|
|
64
|
+
substacks,
|
|
65
|
+
branches,
|
|
66
|
+
impedanceProfiles,
|
|
67
|
+
transmissionLines,
|
|
68
|
+
viaSpans,
|
|
69
|
+
backdrillSpans,
|
|
70
|
+
layerById
|
|
71
|
+
})
|
|
72
|
+
const summary = {
|
|
73
|
+
layerCount: layers.length,
|
|
74
|
+
substackCount: substacks.length,
|
|
75
|
+
boardRegionCount: (input.boardRegions || []).length,
|
|
76
|
+
branchCount: branches.length,
|
|
77
|
+
impedanceProfileCount: impedanceProfiles.length,
|
|
78
|
+
transmissionLineCount: transmissionLines.length,
|
|
79
|
+
viaSpanCount: viaSpans.length,
|
|
80
|
+
backdrillSpanCount: backdrillSpans.length,
|
|
81
|
+
topLevelBendLineCount: topLevelBendLines.length,
|
|
82
|
+
cavityRegionCount: cavityReport.cavityRegionCount,
|
|
83
|
+
stiffenerLayerCount: cavityReport.stiffenerLayerCount,
|
|
84
|
+
adhesiveLayerCount: cavityReport.adhesiveLayerCount,
|
|
85
|
+
diagnosticCount: diagnostics.length
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!Object.values(summary).some((value) => value > 0)) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const readModel = {
|
|
93
|
+
schema: PcbLayerStackReadModelBuilder.SCHEMA_ID,
|
|
94
|
+
summary,
|
|
95
|
+
source: PcbLayerStackReadModelBuilder.#source(input),
|
|
96
|
+
sourceMap: PcbLayerStackSourceMetadataParser.sourceMap(
|
|
97
|
+
layers,
|
|
98
|
+
topLevelBendLines,
|
|
99
|
+
cavityReport
|
|
100
|
+
),
|
|
101
|
+
layers,
|
|
102
|
+
substacks,
|
|
103
|
+
branches,
|
|
104
|
+
topLevelBendLines,
|
|
105
|
+
cavityReport,
|
|
106
|
+
impedanceProfiles,
|
|
107
|
+
transmissionLines,
|
|
108
|
+
viaSpans,
|
|
109
|
+
backdrillSpans,
|
|
110
|
+
diagnostics
|
|
111
|
+
}
|
|
112
|
+
readModel.fidelityReport =
|
|
113
|
+
PcbLayerStackFidelityReportBuilder.build(readModel)
|
|
114
|
+
|
|
115
|
+
return readModel
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Merges board-record field maps for indexed sidecar scans.
|
|
120
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records Board records.
|
|
121
|
+
* @returns {Record<string, string | string[]>}
|
|
122
|
+
*/
|
|
123
|
+
static #mergeFields(records) {
|
|
124
|
+
return Object.assign(
|
|
125
|
+
{},
|
|
126
|
+
...records.map((record) => record.fields || {})
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds source provenance for the sidecar.
|
|
132
|
+
* @param {{ fileName: string, boardRecords: { sourceStream?: string }[], streamNames?: string[], boardRegions?: object[] }} input Source model context.
|
|
133
|
+
* @returns {object}
|
|
134
|
+
*/
|
|
135
|
+
static #source(input) {
|
|
136
|
+
const nativeStreams = [
|
|
137
|
+
...new Set(
|
|
138
|
+
(input.boardRecords || [])
|
|
139
|
+
.map((record) => record.sourceStream)
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
fileName: input.fileName,
|
|
146
|
+
nativeStreams,
|
|
147
|
+
hasNativeBoardData: nativeStreams.includes('Board6/Data'),
|
|
148
|
+
hasBoardRegionsData:
|
|
149
|
+
(input.streamNames || []).includes('BoardRegions/Data') ||
|
|
150
|
+
(input.boardRegions || []).length > 0
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Normalizes stack layers and primitive-layer fallbacks.
|
|
156
|
+
* @param {object[]} layers Parsed physical layers.
|
|
157
|
+
* @param {object[]} primitiveLayers Primitive layer map.
|
|
158
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
159
|
+
* @returns {object[]}
|
|
160
|
+
*/
|
|
161
|
+
static #layers(layers, primitiveLayers, fields) {
|
|
162
|
+
if (layers.length) {
|
|
163
|
+
return layers.map((layer) =>
|
|
164
|
+
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
165
|
+
index: layer.index,
|
|
166
|
+
layerId: layer.layerId,
|
|
167
|
+
layerKey: PcbLayerStackReadModelBuilder.#layerKey(
|
|
168
|
+
layer.layerId
|
|
169
|
+
),
|
|
170
|
+
name: layer.name,
|
|
171
|
+
kind: layer.kind,
|
|
172
|
+
material: layer.material,
|
|
173
|
+
thicknessMil: layer.thicknessMil,
|
|
174
|
+
copperThicknessMil: layer.copperThicknessMil,
|
|
175
|
+
copperWeight: layer.copperWeight,
|
|
176
|
+
dielectricConstant: layer.dielectricConstant,
|
|
177
|
+
dissipationFactor: layer.dissipationFactor,
|
|
178
|
+
...PcbLayerStackSourceMetadataParser.layerSourceFields(
|
|
179
|
+
fields,
|
|
180
|
+
layer.index
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (primitiveLayers || []).map((layer, index) =>
|
|
187
|
+
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
188
|
+
index: index + 1,
|
|
189
|
+
layerId: layer.layerId,
|
|
190
|
+
layerKey: PcbLayerStackReadModelBuilder.#layerKey(
|
|
191
|
+
layer.layerId
|
|
192
|
+
),
|
|
193
|
+
name: layer.name,
|
|
194
|
+
kind: layer.kind || layer.role,
|
|
195
|
+
...PcbLayerStackSourceMetadataParser.layerSourceFields(
|
|
196
|
+
fields,
|
|
197
|
+
index + 1
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Normalizes substacks and links them to board-region rows.
|
|
205
|
+
* @param {object[]} layerSubstacks Parsed substacks.
|
|
206
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
207
|
+
* @param {Map<number, object>} layerById Layer lookup.
|
|
208
|
+
* @param {object[]} boardRegions Board regions.
|
|
209
|
+
* @returns {object[]}
|
|
210
|
+
*/
|
|
211
|
+
static #substacks(layerSubstacks, fields, layerById, boardRegions) {
|
|
212
|
+
return layerSubstacks.map((substack) => {
|
|
213
|
+
const layerIds = PcbLayerStackReadModelBuilder.#layerIdList(
|
|
214
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
215
|
+
fields,
|
|
216
|
+
['V9_SUBSTACK', 'SUBSTACK', 'LAYERSUBSTACK_V8_'],
|
|
217
|
+
substack.index,
|
|
218
|
+
['LAYERS', 'LAYERIDS', 'LAYER_IDS', 'STACKLAYERS']
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
const regions = boardRegions
|
|
222
|
+
.map((region, regionIndex) => ({ region, regionIndex }))
|
|
223
|
+
.filter(
|
|
224
|
+
({ region }) =>
|
|
225
|
+
region.layerStackId &&
|
|
226
|
+
region.layerStackId === substack.id
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
230
|
+
index: substack.index,
|
|
231
|
+
id: substack.id,
|
|
232
|
+
name: substack.name,
|
|
233
|
+
isFlex: substack.isFlex,
|
|
234
|
+
layerIds,
|
|
235
|
+
layerKeys: layerIds
|
|
236
|
+
.map((layerId) => layerById.get(layerId)?.layerKey)
|
|
237
|
+
.filter(Boolean),
|
|
238
|
+
boardRegionIndexes: regions.map(
|
|
239
|
+
({ regionIndex }) => regionIndex
|
|
240
|
+
),
|
|
241
|
+
boardRegionNames: regions
|
|
242
|
+
.map(({ region }) => region.name)
|
|
243
|
+
.filter(Boolean),
|
|
244
|
+
bendingLineCount: regions.reduce(
|
|
245
|
+
(count, { region }) =>
|
|
246
|
+
count + (region.bendingLineCount || 0),
|
|
247
|
+
0
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parses layer-stack branch rows.
|
|
255
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
256
|
+
* @returns {object[]}
|
|
257
|
+
*/
|
|
258
|
+
static #branches(fields) {
|
|
259
|
+
return PcbLayerStackReadModelBuilder.#indexedRows(fields, [
|
|
260
|
+
/^STACKBRANCH(\d+)_ID$/iu,
|
|
261
|
+
/^V9_STACKBRANCH(\d+)_ID$/iu
|
|
262
|
+
]).map((index) =>
|
|
263
|
+
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
264
|
+
index,
|
|
265
|
+
id: PcbLayerStackReadModelBuilder.#indexedField(
|
|
266
|
+
fields,
|
|
267
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
268
|
+
index,
|
|
269
|
+
['ID']
|
|
270
|
+
),
|
|
271
|
+
name: PcbLayerStackReadModelBuilder.#indexedField(
|
|
272
|
+
fields,
|
|
273
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
274
|
+
index,
|
|
275
|
+
['NAME']
|
|
276
|
+
),
|
|
277
|
+
rootStackRef: PcbLayerStackReadModelBuilder.#indexedField(
|
|
278
|
+
fields,
|
|
279
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
280
|
+
index,
|
|
281
|
+
['ROOTSTACKREF', 'ROOT_STACK_REF', 'PARENTSTACKREF']
|
|
282
|
+
),
|
|
283
|
+
stackRefs: PcbLayerStackReadModelBuilder.#list(
|
|
284
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
285
|
+
fields,
|
|
286
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
287
|
+
index,
|
|
288
|
+
['STACKREFS', 'STACK_REFS', 'SECTIONSTACKREFS']
|
|
289
|
+
)
|
|
290
|
+
),
|
|
291
|
+
description: PcbLayerStackReadModelBuilder.#indexedField(
|
|
292
|
+
fields,
|
|
293
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
294
|
+
index,
|
|
295
|
+
['DESCRIPTION']
|
|
296
|
+
),
|
|
297
|
+
parentBranchId: PcbLayerStackReadModelBuilder.#indexedField(
|
|
298
|
+
fields,
|
|
299
|
+
['STACKBRANCH', 'V9_STACKBRANCH'],
|
|
300
|
+
index,
|
|
301
|
+
['PARENTBRANCHID', 'PARENT_BRANCH_ID']
|
|
302
|
+
),
|
|
303
|
+
sections: PcbLayerStackSourceMetadataParser.optionalArray(
|
|
304
|
+
PcbLayerStackSourceMetadataParser.branchSections(
|
|
305
|
+
fields,
|
|
306
|
+
index
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parses impedance profile rows.
|
|
315
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
316
|
+
* @returns {object[]}
|
|
317
|
+
*/
|
|
318
|
+
static #impedanceProfiles(fields) {
|
|
319
|
+
return PcbLayerStackReadModelBuilder.#indexedRows(fields, [
|
|
320
|
+
/^IMPEDANCEPROFILE(\d+)_ID$/iu,
|
|
321
|
+
/^V9_IMPEDANCEPROFILE(\d+)_ID$/iu
|
|
322
|
+
]).map((index) =>
|
|
323
|
+
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
324
|
+
index,
|
|
325
|
+
id: PcbLayerStackReadModelBuilder.#indexedField(
|
|
326
|
+
fields,
|
|
327
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
328
|
+
index,
|
|
329
|
+
['ID']
|
|
330
|
+
),
|
|
331
|
+
name: PcbLayerStackReadModelBuilder.#indexedField(
|
|
332
|
+
fields,
|
|
333
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
334
|
+
index,
|
|
335
|
+
['NAME']
|
|
336
|
+
),
|
|
337
|
+
targetImpedanceOhm: PcbLayerStackReadModelBuilder.#number(
|
|
338
|
+
fields,
|
|
339
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
340
|
+
index,
|
|
341
|
+
['TARGETIMPEDANCE', 'TARGET_IMPEDANCE', 'IMPEDANCE']
|
|
342
|
+
),
|
|
343
|
+
kind: PcbLayerStackReadModelBuilder.#indexedField(
|
|
344
|
+
fields,
|
|
345
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
346
|
+
index,
|
|
347
|
+
['KIND', 'TYPE']
|
|
348
|
+
),
|
|
349
|
+
profileTypeRaw: PcbLayerStackReadModelBuilder.#number(
|
|
350
|
+
fields,
|
|
351
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
352
|
+
index,
|
|
353
|
+
['TYPERAW', 'TYPE_RAW', 'PROFILETYPERAW']
|
|
354
|
+
),
|
|
355
|
+
tolerance: PcbLayerStackReadModelBuilder.#indexedField(
|
|
356
|
+
fields,
|
|
357
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
358
|
+
index,
|
|
359
|
+
['TOLERANCE']
|
|
360
|
+
),
|
|
361
|
+
transmissionLineCount: PcbLayerStackReadModelBuilder.#number(
|
|
362
|
+
fields,
|
|
363
|
+
['IMPEDANCEPROFILE', 'V9_IMPEDANCEPROFILE'],
|
|
364
|
+
index,
|
|
365
|
+
['TRANSMISSIONLINECOUNT', 'TRANSMISSION_LINE_COUNT']
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Parses transmission-line rows tied to impedance profiles.
|
|
373
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
374
|
+
* @param {Map<number, object>} layerById Layer lookup.
|
|
375
|
+
* @returns {object[]}
|
|
376
|
+
*/
|
|
377
|
+
static #transmissionLines(fields, layerById) {
|
|
378
|
+
return PcbLayerStackReadModelBuilder.#indexedRows(fields, [
|
|
379
|
+
/^TRANSMISSIONLINE(\d+)_ID$/iu,
|
|
380
|
+
/^V9_TRANSMISSIONLINE(\d+)_ID$/iu
|
|
381
|
+
]).map((index) => {
|
|
382
|
+
const layerId = PcbLayerStackReadModelBuilder.#number(
|
|
383
|
+
fields,
|
|
384
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
385
|
+
index,
|
|
386
|
+
['LAYERID', 'LAYER_ID', 'SIGNALLAYERID']
|
|
387
|
+
)
|
|
388
|
+
const referenceLayerId = PcbLayerStackReadModelBuilder.#number(
|
|
389
|
+
fields,
|
|
390
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
391
|
+
index,
|
|
392
|
+
[
|
|
393
|
+
'REFERENCE_LAYERID',
|
|
394
|
+
'REFERENCELAYERID',
|
|
395
|
+
'REFERENCE_LAYER_ID',
|
|
396
|
+
'REFERENCELAYER'
|
|
397
|
+
]
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
401
|
+
index,
|
|
402
|
+
id: PcbLayerStackReadModelBuilder.#indexedField(
|
|
403
|
+
fields,
|
|
404
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
405
|
+
index,
|
|
406
|
+
['ID']
|
|
407
|
+
),
|
|
408
|
+
name: PcbLayerStackReadModelBuilder.#indexedField(
|
|
409
|
+
fields,
|
|
410
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
411
|
+
index,
|
|
412
|
+
['NAME']
|
|
413
|
+
),
|
|
414
|
+
profileId: PcbLayerStackReadModelBuilder.#indexedField(
|
|
415
|
+
fields,
|
|
416
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
417
|
+
index,
|
|
418
|
+
['PROFILEID', 'PROFILE_ID', 'IMPEDANCEPROFILEID']
|
|
419
|
+
),
|
|
420
|
+
substackId: PcbLayerStackReadModelBuilder.#indexedField(
|
|
421
|
+
fields,
|
|
422
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
423
|
+
index,
|
|
424
|
+
['SUBSTACKID', 'SUBSTACK_ID']
|
|
425
|
+
),
|
|
426
|
+
layerId,
|
|
427
|
+
layerKey: layerById.get(layerId)?.layerKey,
|
|
428
|
+
referenceLayerId,
|
|
429
|
+
referenceLayerKey: layerById.get(referenceLayerId)?.layerKey,
|
|
430
|
+
topRefId: PcbLayerStackReadModelBuilder.#indexedField(
|
|
431
|
+
fields,
|
|
432
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
433
|
+
index,
|
|
434
|
+
['TOPREFID', 'TOP_REF_ID']
|
|
435
|
+
),
|
|
436
|
+
bottomRefId: PcbLayerStackReadModelBuilder.#indexedField(
|
|
437
|
+
fields,
|
|
438
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
439
|
+
index,
|
|
440
|
+
['BOTTOMREFID', 'BOTTOM_REF_ID']
|
|
441
|
+
),
|
|
442
|
+
widthMil: PcbLayerStackReadModelBuilder.#number(
|
|
443
|
+
fields,
|
|
444
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
445
|
+
index,
|
|
446
|
+
['WIDTH', 'TRACEWIDTH']
|
|
447
|
+
),
|
|
448
|
+
gapMil: PcbLayerStackReadModelBuilder.#number(
|
|
449
|
+
fields,
|
|
450
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
451
|
+
index,
|
|
452
|
+
['GAP', 'PAIRGAP']
|
|
453
|
+
),
|
|
454
|
+
isDifferential: PcbLayerStackReadModelBuilder.#optionalBoolean(
|
|
455
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
456
|
+
fields,
|
|
457
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
458
|
+
index,
|
|
459
|
+
['ISDIFFERENTIAL', 'IS_DIFFERENTIAL']
|
|
460
|
+
)
|
|
461
|
+
),
|
|
462
|
+
calcMode: PcbLayerStackReadModelBuilder.#indexedField(
|
|
463
|
+
fields,
|
|
464
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
465
|
+
index,
|
|
466
|
+
['CALCMODE', 'CALC_MODE']
|
|
467
|
+
),
|
|
468
|
+
calcModeRaw: PcbLayerStackReadModelBuilder.#number(
|
|
469
|
+
fields,
|
|
470
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
471
|
+
index,
|
|
472
|
+
['CALCMODERAW', 'CALC_MODE_RAW']
|
|
473
|
+
),
|
|
474
|
+
impedanceError: PcbLayerStackReadModelBuilder.#indexedField(
|
|
475
|
+
fields,
|
|
476
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
477
|
+
index,
|
|
478
|
+
['IMPEDANCEERROR', 'IMPEDANCE_ERROR']
|
|
479
|
+
),
|
|
480
|
+
tlTypeName: PcbLayerStackReadModelBuilder.#indexedField(
|
|
481
|
+
fields,
|
|
482
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
483
|
+
index,
|
|
484
|
+
['TLTYPENAME', 'TL_TYPE_NAME']
|
|
485
|
+
),
|
|
486
|
+
hasPlating: PcbLayerStackReadModelBuilder.#optionalBoolean(
|
|
487
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
488
|
+
fields,
|
|
489
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
490
|
+
index,
|
|
491
|
+
['HASPLATING', 'HAS_PLATING']
|
|
492
|
+
)
|
|
493
|
+
),
|
|
494
|
+
useSolderMask: PcbLayerStackReadModelBuilder.#optionalBoolean(
|
|
495
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
496
|
+
fields,
|
|
497
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
498
|
+
index,
|
|
499
|
+
['USESOLDERMASK', 'USE_SOLDER_MASK']
|
|
500
|
+
)
|
|
501
|
+
),
|
|
502
|
+
coatedHeight1: PcbLayerStackReadModelBuilder.#indexedField(
|
|
503
|
+
fields,
|
|
504
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
505
|
+
index,
|
|
506
|
+
['COATEDHEIGHT1', 'COATED_HEIGHT_1']
|
|
507
|
+
),
|
|
508
|
+
coatedHeight2: PcbLayerStackReadModelBuilder.#indexedField(
|
|
509
|
+
fields,
|
|
510
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
511
|
+
index,
|
|
512
|
+
['COATEDHEIGHT2', 'COATED_HEIGHT_2']
|
|
513
|
+
),
|
|
514
|
+
clearanceToPlane: PcbLayerStackReadModelBuilder.#indexedField(
|
|
515
|
+
fields,
|
|
516
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
517
|
+
index,
|
|
518
|
+
['CLEARANCETOPLANE', 'CLEARANCE_TO_PLANE']
|
|
519
|
+
),
|
|
520
|
+
electricParameters: PcbLayerStackReadModelBuilder.#keyValueMap(
|
|
521
|
+
PcbLayerStackReadModelBuilder.#indexedField(
|
|
522
|
+
fields,
|
|
523
|
+
['TRANSMISSIONLINE', 'V9_TRANSMISSIONLINE'],
|
|
524
|
+
index,
|
|
525
|
+
['ELECTRICPARAMETERS', 'ELECTRIC_PARAMETERS']
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Parses via/backdrill span rows.
|
|
534
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
535
|
+
* @param {Map<number, object>} layerById Layer lookup.
|
|
536
|
+
* @param {'via' | 'backdrill'} kind Span kind.
|
|
537
|
+
* @returns {object[]}
|
|
538
|
+
*/
|
|
539
|
+
static #layerSpans(fields, layerById, kind) {
|
|
540
|
+
const prefixes =
|
|
541
|
+
kind === 'via'
|
|
542
|
+
? ['VIASPAN', 'V9_VIASPAN']
|
|
543
|
+
: ['BACKDRILLSPAN', 'V9_BACKDRILLSPAN']
|
|
544
|
+
const patterns =
|
|
545
|
+
kind === 'via'
|
|
546
|
+
? [/^VIASPAN(\d+)_ID$/iu, /^V9_VIASPAN(\d+)_ID$/iu]
|
|
547
|
+
: [/^BACKDRILLSPAN(\d+)_ID$/iu, /^V9_BACKDRILLSPAN(\d+)_ID$/iu]
|
|
548
|
+
|
|
549
|
+
return PcbLayerStackReadModelBuilder.#indexedRows(fields, patterns).map(
|
|
550
|
+
(index) => {
|
|
551
|
+
const startLayerId = PcbLayerStackReadModelBuilder.#number(
|
|
552
|
+
fields,
|
|
553
|
+
prefixes,
|
|
554
|
+
index,
|
|
555
|
+
['STARTLAYER', 'STARTLAYERID', 'START_LAYERID', 'FROMLAYER']
|
|
556
|
+
)
|
|
557
|
+
const endLayerId = PcbLayerStackReadModelBuilder.#number(
|
|
558
|
+
fields,
|
|
559
|
+
prefixes,
|
|
560
|
+
index,
|
|
561
|
+
['ENDLAYER', 'ENDLAYERID', 'END_LAYERID', 'TOLAYER']
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
565
|
+
index,
|
|
566
|
+
id: PcbLayerStackReadModelBuilder.#indexedField(
|
|
567
|
+
fields,
|
|
568
|
+
prefixes,
|
|
569
|
+
index,
|
|
570
|
+
['ID']
|
|
571
|
+
),
|
|
572
|
+
name: PcbLayerStackReadModelBuilder.#indexedField(
|
|
573
|
+
fields,
|
|
574
|
+
prefixes,
|
|
575
|
+
index,
|
|
576
|
+
['NAME']
|
|
577
|
+
),
|
|
578
|
+
startLayerId,
|
|
579
|
+
startLayerKey: layerById.get(startLayerId)?.layerKey,
|
|
580
|
+
endLayerId,
|
|
581
|
+
endLayerKey: layerById.get(endLayerId)?.layerKey,
|
|
582
|
+
targetStubMil: PcbLayerStackReadModelBuilder.#number(
|
|
583
|
+
fields,
|
|
584
|
+
prefixes,
|
|
585
|
+
index,
|
|
586
|
+
['TARGETSTUB', 'TARGET_STUB', 'MAXSTUB']
|
|
587
|
+
)
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Builds preservation-first diagnostics for unresolved references.
|
|
595
|
+
* @param {object} input Sidecar sections.
|
|
596
|
+
* @returns {object[]}
|
|
597
|
+
*/
|
|
598
|
+
static #diagnostics(input) {
|
|
599
|
+
const diagnostics = []
|
|
600
|
+
const stackIds = new Set(
|
|
601
|
+
input.substacks.map((substack) => substack.id).filter(Boolean)
|
|
602
|
+
)
|
|
603
|
+
const profileIds = new Set(
|
|
604
|
+
input.impedanceProfiles.map((profile) => profile.id).filter(Boolean)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
for (const branch of input.branches) {
|
|
608
|
+
for (const stackRef of branch.stackRefs || []) {
|
|
609
|
+
if (stackIds.has(stackRef)) continue
|
|
610
|
+
diagnostics.push(
|
|
611
|
+
PcbLayerStackReadModelBuilder.#diagnostic(
|
|
612
|
+
'pcb.layer-stack.unresolved-branch-substack',
|
|
613
|
+
'Layer-stack branch references an unknown substack.',
|
|
614
|
+
{ branchId: branch.id, stackRef }
|
|
615
|
+
)
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
for (const line of input.transmissionLines) {
|
|
621
|
+
if (line.profileId && !profileIds.has(line.profileId)) {
|
|
622
|
+
diagnostics.push(
|
|
623
|
+
PcbLayerStackReadModelBuilder.#diagnostic(
|
|
624
|
+
'pcb.layer-stack.unresolved-impedance-profile',
|
|
625
|
+
'Transmission-line metadata references an unknown impedance profile.',
|
|
626
|
+
{
|
|
627
|
+
transmissionLineId: line.id,
|
|
628
|
+
profileId: line.profileId
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
PcbLayerStackReadModelBuilder.#layerDiagnostic(
|
|
634
|
+
diagnostics,
|
|
635
|
+
input.layerById,
|
|
636
|
+
line.layerId,
|
|
637
|
+
'pcb.layer-stack.unresolved-transmission-layer',
|
|
638
|
+
{ transmissionLineId: line.id, layerRole: 'signal' }
|
|
639
|
+
)
|
|
640
|
+
PcbLayerStackReadModelBuilder.#layerDiagnostic(
|
|
641
|
+
diagnostics,
|
|
642
|
+
input.layerById,
|
|
643
|
+
line.referenceLayerId,
|
|
644
|
+
'pcb.layer-stack.unresolved-reference-layer',
|
|
645
|
+
{ transmissionLineId: line.id, layerRole: 'reference' }
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
for (const span of [...input.viaSpans, ...input.backdrillSpans]) {
|
|
650
|
+
PcbLayerStackReadModelBuilder.#layerDiagnostic(
|
|
651
|
+
diagnostics,
|
|
652
|
+
input.layerById,
|
|
653
|
+
span.startLayerId,
|
|
654
|
+
'pcb.layer-stack.unresolved-span-start-layer',
|
|
655
|
+
{ spanId: span.id }
|
|
656
|
+
)
|
|
657
|
+
PcbLayerStackReadModelBuilder.#layerDiagnostic(
|
|
658
|
+
diagnostics,
|
|
659
|
+
input.layerById,
|
|
660
|
+
span.endLayerId,
|
|
661
|
+
'pcb.layer-stack.unresolved-span-end-layer',
|
|
662
|
+
{ spanId: span.id }
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return diagnostics
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Adds an unresolved-layer diagnostic when needed.
|
|
671
|
+
* @param {object[]} diagnostics Diagnostic target.
|
|
672
|
+
* @param {Map<number, object>} layerById Layer lookup.
|
|
673
|
+
* @param {number | undefined} layerId Layer id.
|
|
674
|
+
* @param {string} code Diagnostic code.
|
|
675
|
+
* @param {object} extra Extra fields.
|
|
676
|
+
* @returns {void}
|
|
677
|
+
*/
|
|
678
|
+
static #layerDiagnostic(diagnostics, layerById, layerId, code, extra) {
|
|
679
|
+
if (!Number.isFinite(layerId) || layerById.has(layerId)) return
|
|
680
|
+
|
|
681
|
+
diagnostics.push(
|
|
682
|
+
PcbLayerStackReadModelBuilder.#diagnostic(
|
|
683
|
+
code,
|
|
684
|
+
'Layer-stack metadata references an unknown layer.',
|
|
685
|
+
{ ...extra, layerId }
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Builds a structured diagnostic row.
|
|
692
|
+
* @param {string} code Diagnostic code.
|
|
693
|
+
* @param {string} message Diagnostic message.
|
|
694
|
+
* @param {object} extra Extra fields.
|
|
695
|
+
* @returns {object}
|
|
696
|
+
*/
|
|
697
|
+
static #diagnostic(code, message, extra) {
|
|
698
|
+
return {
|
|
699
|
+
code,
|
|
700
|
+
severity: 'warning',
|
|
701
|
+
message,
|
|
702
|
+
...extra
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Finds all indexes matching any row-id pattern.
|
|
708
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
709
|
+
* @param {RegExp[]} patterns Index patterns.
|
|
710
|
+
* @returns {number[]}
|
|
711
|
+
*/
|
|
712
|
+
static #indexedRows(fields, patterns) {
|
|
713
|
+
return [
|
|
714
|
+
...new Set(
|
|
715
|
+
Object.keys(fields).flatMap((key) => {
|
|
716
|
+
for (const pattern of patterns) {
|
|
717
|
+
const match = pattern.exec(key)
|
|
718
|
+
if (match) return [Number.parseInt(match[1], 10)]
|
|
719
|
+
}
|
|
720
|
+
return []
|
|
721
|
+
})
|
|
722
|
+
)
|
|
723
|
+
].sort((left, right) => left - right)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Finds nested indexes for fields with a common prefix and suffix.
|
|
728
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
729
|
+
* @param {string} prefix Field prefix before the nested index.
|
|
730
|
+
* @param {string} suffix Field suffix after the nested index.
|
|
731
|
+
* @returns {number[]}
|
|
732
|
+
*/
|
|
733
|
+
static #nestedIndexes(fields, prefix, suffix) {
|
|
734
|
+
const pattern = new RegExp(
|
|
735
|
+
'^' +
|
|
736
|
+
prefix.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') +
|
|
737
|
+
'(\\d+)' +
|
|
738
|
+
suffix.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') +
|
|
739
|
+
'$',
|
|
740
|
+
'iu'
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
return [
|
|
744
|
+
...new Set(
|
|
745
|
+
Object.keys(fields).flatMap((key) => {
|
|
746
|
+
const match = pattern.exec(key)
|
|
747
|
+
return match ? [Number.parseInt(match[1], 10)] : []
|
|
748
|
+
})
|
|
749
|
+
)
|
|
750
|
+
].sort((left, right) => left - right)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Reads the first matching indexed field.
|
|
755
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
756
|
+
* @param {string[]} prefixes Row prefixes.
|
|
757
|
+
* @param {number} index Row index.
|
|
758
|
+
* @param {string[]} suffixes Field suffixes.
|
|
759
|
+
* @returns {string}
|
|
760
|
+
*/
|
|
761
|
+
static #indexedField(fields, prefixes, index, suffixes) {
|
|
762
|
+
for (const prefix of prefixes) {
|
|
763
|
+
for (const suffix of suffixes) {
|
|
764
|
+
const value = PcbLayerStackReadModelBuilder.#field(
|
|
765
|
+
fields,
|
|
766
|
+
prefix + index + '_' + suffix
|
|
767
|
+
)
|
|
768
|
+
if (value) return value
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return ''
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Parses one indexed numeric field.
|
|
777
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
778
|
+
* @param {string[]} prefixes Row prefixes.
|
|
779
|
+
* @param {number} index Row index.
|
|
780
|
+
* @param {string[]} suffixes Field suffixes.
|
|
781
|
+
* @returns {number | undefined}
|
|
782
|
+
*/
|
|
783
|
+
static #number(fields, prefixes, index, suffixes) {
|
|
784
|
+
for (const prefix of prefixes) {
|
|
785
|
+
for (const suffix of suffixes) {
|
|
786
|
+
const key = prefix + index + '_' + suffix
|
|
787
|
+
const parsed = parseNumericField(fields, key)
|
|
788
|
+
if (parsed !== null) return parsed
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return undefined
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Reads a case-insensitive field value.
|
|
797
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
798
|
+
* @param {string} key Field key.
|
|
799
|
+
* @returns {string}
|
|
800
|
+
*/
|
|
801
|
+
static #field(fields, key) {
|
|
802
|
+
if (Object.hasOwn(fields, key)) {
|
|
803
|
+
return ParserUtils.getField(fields, key)
|
|
804
|
+
}
|
|
805
|
+
const upperKey = key.toUpperCase()
|
|
806
|
+
const realKey = Object.keys(fields).find(
|
|
807
|
+
(fieldKey) => fieldKey.toUpperCase() === upperKey
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Parses layer-id lists.
|
|
815
|
+
* @param {string} value Raw list value.
|
|
816
|
+
* @returns {number[]}
|
|
817
|
+
*/
|
|
818
|
+
static #layerIdList(value) {
|
|
819
|
+
return PcbLayerStackReadModelBuilder.#list(value)
|
|
820
|
+
.map((item) => Number.parseInt(item, 10))
|
|
821
|
+
.filter(Number.isFinite)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Splits a native list field.
|
|
826
|
+
* @param {string} value Raw list value.
|
|
827
|
+
* @returns {string[]}
|
|
828
|
+
*/
|
|
829
|
+
static #list(value) {
|
|
830
|
+
return String(value || '')
|
|
831
|
+
.split(/[;,]/u)
|
|
832
|
+
.map((item) => item.trim())
|
|
833
|
+
.filter(Boolean)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Parses a native key-value property bag.
|
|
838
|
+
* @param {string} value Raw value.
|
|
839
|
+
* @returns {object | undefined}
|
|
840
|
+
*/
|
|
841
|
+
static #keyValueMap(value) {
|
|
842
|
+
const entries = String(value || '')
|
|
843
|
+
.split(/[|;]/u)
|
|
844
|
+
.map((item) => item.trim())
|
|
845
|
+
.filter(Boolean)
|
|
846
|
+
.flatMap((item) => {
|
|
847
|
+
const separator = item.indexOf('=')
|
|
848
|
+
if (separator < 0) return []
|
|
849
|
+
return [
|
|
850
|
+
[
|
|
851
|
+
item.slice(0, separator).trim(),
|
|
852
|
+
item.slice(separator + 1).trim()
|
|
853
|
+
]
|
|
854
|
+
]
|
|
855
|
+
})
|
|
856
|
+
.filter(([key]) => key)
|
|
857
|
+
|
|
858
|
+
return entries.length ? Object.fromEntries(entries) : undefined
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Parses an optional boolean value.
|
|
863
|
+
* @param {string} value Raw value.
|
|
864
|
+
* @returns {boolean | undefined}
|
|
865
|
+
*/
|
|
866
|
+
static #optionalBoolean(value) {
|
|
867
|
+
const normalized = String(value || '')
|
|
868
|
+
.trim()
|
|
869
|
+
.toLowerCase()
|
|
870
|
+
if (!normalized) return undefined
|
|
871
|
+
return ['true', 't', '1', 'yes'].includes(normalized)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Parses a numeric token.
|
|
876
|
+
* @param {string | undefined} value Raw token.
|
|
877
|
+
* @returns {number | undefined}
|
|
878
|
+
*/
|
|
879
|
+
static #numberToken(value) {
|
|
880
|
+
const parsed = Number.parseFloat(String(value || '').trim())
|
|
881
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Builds a stable layer key.
|
|
886
|
+
* @param {number | null | undefined} layerId Layer id.
|
|
887
|
+
* @returns {string | undefined}
|
|
888
|
+
*/
|
|
889
|
+
static #layerKey(layerId) {
|
|
890
|
+
return Number.isFinite(layerId) ? 'L' + layerId : undefined
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Removes undefined and empty string values while keeping false and empty
|
|
895
|
+
* arrays stable.
|
|
896
|
+
* @param {Record<string, unknown>} object Source object.
|
|
897
|
+
* @returns {object}
|
|
898
|
+
*/
|
|
899
|
+
static #stripUndefined(object) {
|
|
900
|
+
return Object.fromEntries(
|
|
901
|
+
Object.entries(object).filter(
|
|
902
|
+
([, value]) => value !== undefined && value !== ''
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
}
|
|
906
|
+
}
|