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,488 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses source-only layer-stack metadata that is not part of core geometry.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbLayerStackSourceMetadataParser {
|
|
11
|
+
/**
|
|
12
|
+
* Parses source-aware extras for one layer-stack row.
|
|
13
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
14
|
+
* @param {number} index Layer-stack index.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static layerSourceFields(fields, index) {
|
|
18
|
+
const prefixes = ['V9_STACK_LAYER', 'STACK_LAYER']
|
|
19
|
+
const layerField = (suffixes) =>
|
|
20
|
+
PcbLayerStackSourceMetadataParser.#indexedField(
|
|
21
|
+
fields,
|
|
22
|
+
prefixes,
|
|
23
|
+
index,
|
|
24
|
+
suffixes
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return PcbLayerStackSourceMetadataParser.#stripUndefined({
|
|
28
|
+
family: layerField(['FAMILY', 'LAYERFAMILY']),
|
|
29
|
+
sourceFamily: layerField(['SOURCEFAMILY', 'SOURCE_FAMILY']),
|
|
30
|
+
sourceRecordId: layerField([
|
|
31
|
+
'SOURCE_RECORD_ID',
|
|
32
|
+
'SOURCERECORDID',
|
|
33
|
+
'RECORDID'
|
|
34
|
+
]),
|
|
35
|
+
sourceKeys: PcbLayerStackSourceMetadataParser.optionalArray(
|
|
36
|
+
PcbLayerStackSourceMetadataParser.#list(
|
|
37
|
+
layerField(['SOURCE_KEYS', 'SOURCEKEYS'])
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
registryRef: layerField(['REGISTRYREF', 'REGISTRY_REF']),
|
|
41
|
+
modelId: layerField(['MODELID', 'MODEL_ID']),
|
|
42
|
+
aliases: PcbLayerStackSourceMetadataParser.optionalArray(
|
|
43
|
+
PcbLayerStackSourceMetadataParser.#list(
|
|
44
|
+
layerField(['ALIASES', 'DISPLAYALIASES'])
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
materialColor: layerField(['MATERIALCOLOR', 'MATERIAL_COLOR']),
|
|
48
|
+
surfaceFinish: layerField(['SURFACEFINISH', 'SURFACE_FINISH']),
|
|
49
|
+
plating: layerField(['PLATING']),
|
|
50
|
+
coverlayExpansion: layerField([
|
|
51
|
+
'COVERLAYEXPANSION',
|
|
52
|
+
'COVERLAY_EXPANSION'
|
|
53
|
+
]),
|
|
54
|
+
isStiffener: PcbLayerStackSourceMetadataParser.#optionalBoolean(
|
|
55
|
+
layerField(['ISSTIFFENER', 'IS_STIFFENER'])
|
|
56
|
+
),
|
|
57
|
+
isAdhesive: PcbLayerStackSourceMetadataParser.#optionalBoolean(
|
|
58
|
+
layerField(['ISADHESIVE', 'IS_ADHESIVE'])
|
|
59
|
+
),
|
|
60
|
+
stackupxShared: PcbLayerStackSourceMetadataParser.#optionalBoolean(
|
|
61
|
+
layerField(['SHARED', 'STACKUPX_SHARED'])
|
|
62
|
+
),
|
|
63
|
+
stackupxProperties: PcbLayerStackSourceMetadataParser.#keyValueMap(
|
|
64
|
+
layerField(['STACKUPX_PROPERTIES', 'PROPERTIES'])
|
|
65
|
+
),
|
|
66
|
+
substackEnablement: PcbLayerStackSourceMetadataParser.optionalArray(
|
|
67
|
+
PcbLayerStackSourceMetadataParser.#substackEnablement(
|
|
68
|
+
fields,
|
|
69
|
+
index
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns undefined for optional empty arrays.
|
|
77
|
+
* @param {object[]} values Source values.
|
|
78
|
+
* @returns {object[] | undefined}
|
|
79
|
+
*/
|
|
80
|
+
static optionalArray(values) {
|
|
81
|
+
return values.length ? values : undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parses branch-section rows.
|
|
86
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
87
|
+
* @param {number} branchIndex Branch index.
|
|
88
|
+
* @returns {object[]}
|
|
89
|
+
*/
|
|
90
|
+
static branchSections(fields, branchIndex) {
|
|
91
|
+
const sectionIndexes = PcbLayerStackSourceMetadataParser.#nestedIndexes(
|
|
92
|
+
fields,
|
|
93
|
+
'STACKBRANCH' + branchIndex + '_SECTION',
|
|
94
|
+
'_ID'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return sectionIndexes.map((sectionIndex) => {
|
|
98
|
+
const prefix =
|
|
99
|
+
'STACKBRANCH' + branchIndex + '_SECTION' + sectionIndex
|
|
100
|
+
|
|
101
|
+
return PcbLayerStackSourceMetadataParser.#stripUndefined({
|
|
102
|
+
index: sectionIndex,
|
|
103
|
+
id: PcbLayerStackSourceMetadataParser.#field(
|
|
104
|
+
fields,
|
|
105
|
+
prefix + '_ID'
|
|
106
|
+
),
|
|
107
|
+
name: PcbLayerStackSourceMetadataParser.#field(
|
|
108
|
+
fields,
|
|
109
|
+
prefix + '_NAME'
|
|
110
|
+
),
|
|
111
|
+
parentSectionId: PcbLayerStackSourceMetadataParser.#field(
|
|
112
|
+
fields,
|
|
113
|
+
prefix + '_PARENTID'
|
|
114
|
+
),
|
|
115
|
+
stacks: PcbLayerStackSourceMetadataParser.#branchSectionStacks(
|
|
116
|
+
fields,
|
|
117
|
+
prefix
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parses top-level board bend-line cache entries.
|
|
125
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
126
|
+
* @returns {object[]}
|
|
127
|
+
*/
|
|
128
|
+
static topLevelBendLines(fields) {
|
|
129
|
+
return PcbLayerStackSourceMetadataParser.#indexedRows(fields, [
|
|
130
|
+
/^BOARD_BENDLINE(\d+)$/iu,
|
|
131
|
+
/^BENDLINE(\d+)$/iu
|
|
132
|
+
]).map((index) => {
|
|
133
|
+
const raw =
|
|
134
|
+
PcbLayerStackSourceMetadataParser.#field(
|
|
135
|
+
fields,
|
|
136
|
+
'BOARD_BENDLINE' + index
|
|
137
|
+
) ||
|
|
138
|
+
PcbLayerStackSourceMetadataParser.#field(
|
|
139
|
+
fields,
|
|
140
|
+
'BENDLINE' + index
|
|
141
|
+
)
|
|
142
|
+
const tokens = raw.split(';').map((token) => token.trim())
|
|
143
|
+
const radiusRaw = PcbLayerStackSourceMetadataParser.#numberToken(
|
|
144
|
+
tokens[1]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return PcbLayerStackSourceMetadataParser.#stripUndefined({
|
|
148
|
+
index,
|
|
149
|
+
raw,
|
|
150
|
+
angleDeg: PcbLayerStackSourceMetadataParser.#numberToken(
|
|
151
|
+
tokens[0]
|
|
152
|
+
),
|
|
153
|
+
radiusRaw,
|
|
154
|
+
radiusMil:
|
|
155
|
+
radiusRaw === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: Number((radiusRaw / 10000).toFixed(6)),
|
|
158
|
+
foldIndex: PcbLayerStackSourceMetadataParser.#numberToken(
|
|
159
|
+
tokens[2]
|
|
160
|
+
),
|
|
161
|
+
name: tokens[7],
|
|
162
|
+
stateRaw: tokens[8],
|
|
163
|
+
regionName: tokens[9]
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Builds cavity/stiffener reporting metadata.
|
|
170
|
+
* @param {object[]} layers Layer rows.
|
|
171
|
+
* @param {object[]} boardRegions Board-region rows.
|
|
172
|
+
* @returns {object}
|
|
173
|
+
*/
|
|
174
|
+
static cavityReport(layers, boardRegions) {
|
|
175
|
+
const cavityRegions = boardRegions
|
|
176
|
+
.map((region, regionIndex) => ({ region, regionIndex }))
|
|
177
|
+
.filter(({ region }) => Boolean(region.cavityHeight))
|
|
178
|
+
.map(({ region, regionIndex }) =>
|
|
179
|
+
PcbLayerStackSourceMetadataParser.#stripUndefined({
|
|
180
|
+
regionIndex,
|
|
181
|
+
name: region.name,
|
|
182
|
+
layerStackId: region.layerStackId,
|
|
183
|
+
cavityHeight: region.cavityHeight
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
const stiffenerLayers = layers
|
|
187
|
+
.filter((layer) => layer.isStiffener)
|
|
188
|
+
.map((layer) => layer.name)
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
const adhesiveLayers = layers
|
|
191
|
+
.filter((layer) => layer.isAdhesive)
|
|
192
|
+
.map((layer) => layer.name)
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
cavityRegionCount: cavityRegions.length,
|
|
197
|
+
stiffenerLayerCount: stiffenerLayers.length,
|
|
198
|
+
adhesiveLayerCount: adhesiveLayers.length,
|
|
199
|
+
cavityRegions,
|
|
200
|
+
stiffenerLayers,
|
|
201
|
+
adhesiveLayers
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds a compact source-evidence summary.
|
|
207
|
+
* @param {object[]} layers Layer rows.
|
|
208
|
+
* @param {object[]} topLevelBendLines Top-level bend cache rows.
|
|
209
|
+
* @param {object} cavityReport Cavity/stiffener report.
|
|
210
|
+
* @returns {object}
|
|
211
|
+
*/
|
|
212
|
+
static sourceMap(layers, topLevelBendLines, cavityReport) {
|
|
213
|
+
return {
|
|
214
|
+
registryEntryCount: layers.filter((layer) => layer.registryRef)
|
|
215
|
+
.length,
|
|
216
|
+
sourceKeyCount: layers.reduce(
|
|
217
|
+
(count, layer) => count + (layer.sourceKeys?.length || 0),
|
|
218
|
+
0
|
|
219
|
+
),
|
|
220
|
+
topLevelBendLineCount: topLevelBendLines.length,
|
|
221
|
+
cavityRegionCount: cavityReport.cavityRegionCount,
|
|
222
|
+
stiffenerLayerCount: cavityReport.stiffenerLayerCount,
|
|
223
|
+
adhesiveLayerCount: cavityReport.adhesiveLayerCount,
|
|
224
|
+
surfaceFinishCount: layers.filter((layer) => layer.surfaceFinish)
|
|
225
|
+
.length
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Parses per-substack enablement fields from one layer row.
|
|
231
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
232
|
+
* @param {number} layerIndex Layer-stack index.
|
|
233
|
+
* @returns {object[]}
|
|
234
|
+
*/
|
|
235
|
+
static #substackEnablement(fields, layerIndex) {
|
|
236
|
+
const pattern = new RegExp(
|
|
237
|
+
'^V9_STACK_LAYER' + layerIndex + '_SUBSTACK(\\d+)_ENABLED$',
|
|
238
|
+
'iu'
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return Object.keys(fields)
|
|
242
|
+
.flatMap((key) => {
|
|
243
|
+
const match = pattern.exec(key)
|
|
244
|
+
if (!match) return []
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
substackIndex: Number.parseInt(match[1], 10),
|
|
249
|
+
enabled:
|
|
250
|
+
PcbLayerStackSourceMetadataParser.#optionalBoolean(
|
|
251
|
+
PcbLayerStackSourceMetadataParser.#field(
|
|
252
|
+
fields,
|
|
253
|
+
key
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
})
|
|
259
|
+
.sort((left, right) => left.substackIndex - right.substackIndex)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Parses branch-section stack rows.
|
|
264
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
265
|
+
* @param {string} sectionPrefix Section field prefix.
|
|
266
|
+
* @returns {object[]}
|
|
267
|
+
*/
|
|
268
|
+
static #branchSectionStacks(fields, sectionPrefix) {
|
|
269
|
+
const stackIndexes = PcbLayerStackSourceMetadataParser.#nestedIndexes(
|
|
270
|
+
fields,
|
|
271
|
+
sectionPrefix + '_STACK',
|
|
272
|
+
'_REF'
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return stackIndexes.map((stackIndex) => {
|
|
276
|
+
const prefix = sectionPrefix + '_STACK' + stackIndex
|
|
277
|
+
|
|
278
|
+
return PcbLayerStackSourceMetadataParser.#stripUndefined({
|
|
279
|
+
index: stackIndex,
|
|
280
|
+
stackRef: PcbLayerStackSourceMetadataParser.#field(
|
|
281
|
+
fields,
|
|
282
|
+
prefix + '_REF'
|
|
283
|
+
),
|
|
284
|
+
materialUsage: PcbLayerStackSourceMetadataParser.#field(
|
|
285
|
+
fields,
|
|
286
|
+
prefix + '_MATERIALUSAGE'
|
|
287
|
+
),
|
|
288
|
+
source: PcbLayerStackSourceMetadataParser.#field(
|
|
289
|
+
fields,
|
|
290
|
+
prefix + '_SOURCE'
|
|
291
|
+
),
|
|
292
|
+
parentLayerId: PcbLayerStackSourceMetadataParser.#field(
|
|
293
|
+
fields,
|
|
294
|
+
prefix + '_PARENTLAYERID'
|
|
295
|
+
),
|
|
296
|
+
parentLayerStackId: PcbLayerStackSourceMetadataParser.#field(
|
|
297
|
+
fields,
|
|
298
|
+
prefix + '_PARENTLAYERSTACKID'
|
|
299
|
+
),
|
|
300
|
+
sourceLayerId: PcbLayerStackSourceMetadataParser.#field(
|
|
301
|
+
fields,
|
|
302
|
+
prefix + '_SOURCELAYERID'
|
|
303
|
+
),
|
|
304
|
+
sourceLayerStackId: PcbLayerStackSourceMetadataParser.#field(
|
|
305
|
+
fields,
|
|
306
|
+
prefix + '_SOURCELAYERSTACKID'
|
|
307
|
+
),
|
|
308
|
+
intrusionLeftBottom: PcbLayerStackSourceMetadataParser.#field(
|
|
309
|
+
fields,
|
|
310
|
+
prefix + '_INTRUSIONLEFTBOTTOM'
|
|
311
|
+
),
|
|
312
|
+
intrusionLeftTop: PcbLayerStackSourceMetadataParser.#field(
|
|
313
|
+
fields,
|
|
314
|
+
prefix + '_INTRUSIONLEFTTOP'
|
|
315
|
+
),
|
|
316
|
+
intrusionRightBottom: PcbLayerStackSourceMetadataParser.#field(
|
|
317
|
+
fields,
|
|
318
|
+
prefix + '_INTRUSIONRIGHTBOTTOM'
|
|
319
|
+
),
|
|
320
|
+
intrusionRightTop: PcbLayerStackSourceMetadataParser.#field(
|
|
321
|
+
fields,
|
|
322
|
+
prefix + '_INTRUSIONRIGHTTOP'
|
|
323
|
+
)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Finds all indexes matching any row-id pattern.
|
|
330
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
331
|
+
* @param {RegExp[]} patterns Index patterns.
|
|
332
|
+
* @returns {number[]}
|
|
333
|
+
*/
|
|
334
|
+
static #indexedRows(fields, patterns) {
|
|
335
|
+
return [
|
|
336
|
+
...new Set(
|
|
337
|
+
Object.keys(fields).flatMap((key) => {
|
|
338
|
+
for (const pattern of patterns) {
|
|
339
|
+
const match = pattern.exec(key)
|
|
340
|
+
if (match) return [Number.parseInt(match[1], 10)]
|
|
341
|
+
}
|
|
342
|
+
return []
|
|
343
|
+
})
|
|
344
|
+
)
|
|
345
|
+
].sort((left, right) => left - right)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Finds nested indexes for fields with a common prefix and suffix.
|
|
350
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
351
|
+
* @param {string} prefix Field prefix before the nested index.
|
|
352
|
+
* @param {string} suffix Field suffix after the nested index.
|
|
353
|
+
* @returns {number[]}
|
|
354
|
+
*/
|
|
355
|
+
static #nestedIndexes(fields, prefix, suffix) {
|
|
356
|
+
const pattern = new RegExp(
|
|
357
|
+
'^' +
|
|
358
|
+
prefix.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') +
|
|
359
|
+
'(\\d+)' +
|
|
360
|
+
suffix.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') +
|
|
361
|
+
'$',
|
|
362
|
+
'iu'
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return [
|
|
366
|
+
...new Set(
|
|
367
|
+
Object.keys(fields).flatMap((key) => {
|
|
368
|
+
const match = pattern.exec(key)
|
|
369
|
+
return match ? [Number.parseInt(match[1], 10)] : []
|
|
370
|
+
})
|
|
371
|
+
)
|
|
372
|
+
].sort((left, right) => left - right)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Reads the first matching indexed field.
|
|
377
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
378
|
+
* @param {string[]} prefixes Row prefixes.
|
|
379
|
+
* @param {number} index Row index.
|
|
380
|
+
* @param {string[]} suffixes Field suffixes.
|
|
381
|
+
* @returns {string}
|
|
382
|
+
*/
|
|
383
|
+
static #indexedField(fields, prefixes, index, suffixes) {
|
|
384
|
+
for (const prefix of prefixes) {
|
|
385
|
+
for (const suffix of suffixes) {
|
|
386
|
+
const value = PcbLayerStackSourceMetadataParser.#field(
|
|
387
|
+
fields,
|
|
388
|
+
prefix + index + '_' + suffix
|
|
389
|
+
)
|
|
390
|
+
if (value) return value
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return ''
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Reads a case-insensitive field value.
|
|
399
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
400
|
+
* @param {string} key Field key.
|
|
401
|
+
* @returns {string}
|
|
402
|
+
*/
|
|
403
|
+
static #field(fields, key) {
|
|
404
|
+
if (Object.hasOwn(fields, key)) {
|
|
405
|
+
return ParserUtils.getField(fields, key)
|
|
406
|
+
}
|
|
407
|
+
const upperKey = key.toUpperCase()
|
|
408
|
+
const realKey = Object.keys(fields).find(
|
|
409
|
+
(fieldKey) => fieldKey.toUpperCase() === upperKey
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Splits a native list field.
|
|
417
|
+
* @param {string} value Raw list value.
|
|
418
|
+
* @returns {string[]}
|
|
419
|
+
*/
|
|
420
|
+
static #list(value) {
|
|
421
|
+
return String(value || '')
|
|
422
|
+
.split(/[;,]/u)
|
|
423
|
+
.map((item) => item.trim())
|
|
424
|
+
.filter(Boolean)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Parses a native key-value property bag.
|
|
429
|
+
* @param {string} value Raw value.
|
|
430
|
+
* @returns {object | undefined}
|
|
431
|
+
*/
|
|
432
|
+
static #keyValueMap(value) {
|
|
433
|
+
const entries = String(value || '')
|
|
434
|
+
.split(/[|;]/u)
|
|
435
|
+
.map((item) => item.trim())
|
|
436
|
+
.filter(Boolean)
|
|
437
|
+
.flatMap((item) => {
|
|
438
|
+
const separator = item.indexOf('=')
|
|
439
|
+
if (separator < 0) return []
|
|
440
|
+
return [
|
|
441
|
+
[
|
|
442
|
+
item.slice(0, separator).trim(),
|
|
443
|
+
item.slice(separator + 1).trim()
|
|
444
|
+
]
|
|
445
|
+
]
|
|
446
|
+
})
|
|
447
|
+
.filter(([key]) => key)
|
|
448
|
+
|
|
449
|
+
return entries.length ? Object.fromEntries(entries) : undefined
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Parses an optional boolean value.
|
|
454
|
+
* @param {string} value Raw value.
|
|
455
|
+
* @returns {boolean | undefined}
|
|
456
|
+
*/
|
|
457
|
+
static #optionalBoolean(value) {
|
|
458
|
+
const normalized = String(value || '')
|
|
459
|
+
.trim()
|
|
460
|
+
.toLowerCase()
|
|
461
|
+
if (!normalized) return undefined
|
|
462
|
+
return ['true', 't', '1', 'yes'].includes(normalized)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Parses a numeric token.
|
|
467
|
+
* @param {string | undefined} value Raw token.
|
|
468
|
+
* @returns {number | undefined}
|
|
469
|
+
*/
|
|
470
|
+
static #numberToken(value) {
|
|
471
|
+
const parsed = Number.parseFloat(String(value || '').trim())
|
|
472
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Removes undefined and empty string values while keeping false and empty
|
|
477
|
+
* arrays stable.
|
|
478
|
+
* @param {Record<string, unknown>} object Source object.
|
|
479
|
+
* @returns {object}
|
|
480
|
+
*/
|
|
481
|
+
static #stripUndefined(object) {
|
|
482
|
+
return Object.fromEntries(
|
|
483
|
+
Object.entries(object).filter(
|
|
484
|
+
([, value]) => value !== undefined && value !== ''
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -8,6 +8,7 @@ import { LibraryRenderManifestBuilder } from './LibraryRenderManifestBuilder.mjs
|
|
|
8
8
|
import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
|
|
9
9
|
import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
|
|
10
10
|
import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
|
|
11
|
+
import { PcbLibParityReportBuilder } from './PcbLibParityReportBuilder.mjs'
|
|
11
12
|
import { PcbMaskPasteResolver } from './PcbMaskPasteResolver.mjs'
|
|
12
13
|
|
|
13
14
|
const { stripExtension } = ParserUtils
|
|
@@ -74,6 +75,7 @@ export class PcbLibModelParser {
|
|
|
74
75
|
}
|
|
75
76
|
pcbLibrary.renderManifest =
|
|
76
77
|
LibraryRenderManifestBuilder.buildPcbLibraryManifest(pcbLibrary)
|
|
78
|
+
pcbLibrary.parityReport = PcbLibParityReportBuilder.build(pcbLibrary)
|
|
77
79
|
|
|
78
80
|
return NormalizedModelSchema.attach({
|
|
79
81
|
kind: 'pcb-library',
|