altium-toolkit 1.0.7 → 1.0.9
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/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves effective paste/solder mask expansion from primitive, sidecar,
|
|
7
|
+
* rule, and document-default sources.
|
|
8
|
+
*/
|
|
9
|
+
export class PcbMaskPasteResolver {
|
|
10
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.mask-paste.a1'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds and attaches effective mask/paste metadata.
|
|
14
|
+
* @param {{ pads?: object[], vias?: object[], rules?: object[], defaults?: object | null }} context Resolver context.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static build(context) {
|
|
18
|
+
const pads = Array.isArray(context?.pads) ? context.pads : []
|
|
19
|
+
const vias = Array.isArray(context?.vias) ? context.vias : []
|
|
20
|
+
const defaults = context?.defaults || null
|
|
21
|
+
const rules = Array.isArray(context?.rules) ? context.rules : []
|
|
22
|
+
const primitiveRows = [
|
|
23
|
+
...PcbMaskPasteResolver.#resolveFamily(
|
|
24
|
+
'pad',
|
|
25
|
+
pads,
|
|
26
|
+
defaults,
|
|
27
|
+
rules
|
|
28
|
+
),
|
|
29
|
+
...PcbMaskPasteResolver.#resolveFamily('via', vias, defaults, rules)
|
|
30
|
+
]
|
|
31
|
+
const summary = PcbMaskPasteResolver.#summarize(primitiveRows)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
schema: PcbMaskPasteResolver.SCHEMA_ID,
|
|
35
|
+
summary,
|
|
36
|
+
defaults: defaults?.maskPaste || {},
|
|
37
|
+
rules: PcbMaskPasteResolver.#maskRules(rules),
|
|
38
|
+
primitives: primitiveRows
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves one primitive family and attaches primitive metadata.
|
|
44
|
+
* @param {'pad' | 'via'} primitiveKind Primitive kind.
|
|
45
|
+
* @param {object[]} primitives Family primitives.
|
|
46
|
+
* @param {object | null} defaults Board defaults.
|
|
47
|
+
* @param {object[]} rules Parsed rule rows.
|
|
48
|
+
* @returns {object[]}
|
|
49
|
+
*/
|
|
50
|
+
static #resolveFamily(primitiveKind, primitives, defaults, rules) {
|
|
51
|
+
return primitives.flatMap((primitive, index) => {
|
|
52
|
+
const primitiveIndex = Number.isInteger(primitive.primitiveIndex)
|
|
53
|
+
? primitive.primitiveIndex
|
|
54
|
+
: index
|
|
55
|
+
const effectiveMaskPaste = {
|
|
56
|
+
paste: PcbMaskPasteResolver.#resolveSide(
|
|
57
|
+
primitive,
|
|
58
|
+
'paste',
|
|
59
|
+
defaults,
|
|
60
|
+
rules
|
|
61
|
+
),
|
|
62
|
+
solder: PcbMaskPasteResolver.#resolveSide(
|
|
63
|
+
primitive,
|
|
64
|
+
'solder',
|
|
65
|
+
defaults,
|
|
66
|
+
rules
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
effectiveMaskPaste.paste.source === 'unresolved' &&
|
|
72
|
+
effectiveMaskPaste.solder.source === 'unresolved'
|
|
73
|
+
) {
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
primitive.effectiveMaskPaste = effectiveMaskPaste
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
primitiveKey: primitiveKind + '-' + String(primitiveIndex),
|
|
82
|
+
primitiveKind,
|
|
83
|
+
primitiveIndex,
|
|
84
|
+
...effectiveMaskPaste
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves one mask side with source precedence.
|
|
92
|
+
* @param {object} primitive Primitive row.
|
|
93
|
+
* @param {'paste' | 'solder'} side Mask side.
|
|
94
|
+
* @param {object | null} defaults Board defaults.
|
|
95
|
+
* @param {object[]} rules Parsed rules.
|
|
96
|
+
* @returns {object}
|
|
97
|
+
*/
|
|
98
|
+
static #resolveSide(primitive, side, defaults, rules) {
|
|
99
|
+
const sidecar = PcbMaskPasteResolver.#sidecarValue(primitive, side)
|
|
100
|
+
if (sidecar) return sidecar
|
|
101
|
+
|
|
102
|
+
const primitiveValue = PcbMaskPasteResolver.#primitiveValue(
|
|
103
|
+
primitive,
|
|
104
|
+
side
|
|
105
|
+
)
|
|
106
|
+
if (primitiveValue) return primitiveValue
|
|
107
|
+
|
|
108
|
+
const ruleValue = PcbMaskPasteResolver.#ruleValue(rules, side)
|
|
109
|
+
if (ruleValue) return ruleValue
|
|
110
|
+
|
|
111
|
+
const defaultExpansion = PcbMaskPasteResolver.#defaultExpansion(
|
|
112
|
+
defaults,
|
|
113
|
+
side
|
|
114
|
+
)
|
|
115
|
+
if (defaultExpansion !== null) {
|
|
116
|
+
return {
|
|
117
|
+
source: 'document-default',
|
|
118
|
+
expansionMil: defaultExpansion,
|
|
119
|
+
unit: 'mil'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
source: 'unresolved',
|
|
125
|
+
expansionMil: null,
|
|
126
|
+
unit: 'mil'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns a manual sidecar value when present.
|
|
132
|
+
* @param {object} primitive Primitive row.
|
|
133
|
+
* @param {'paste' | 'solder'} side Mask side.
|
|
134
|
+
* @returns {object | null}
|
|
135
|
+
*/
|
|
136
|
+
static #sidecarValue(primitive, side) {
|
|
137
|
+
const sidecar =
|
|
138
|
+
primitive?.extendedPrimitiveInformation?.maskExpansion?.[side]
|
|
139
|
+
if (
|
|
140
|
+
sidecar?.manualExpansion === null ||
|
|
141
|
+
sidecar?.manualExpansion === undefined
|
|
142
|
+
) {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manualExpansion = Number(sidecar.manualExpansion)
|
|
147
|
+
|
|
148
|
+
if (Number.isFinite(manualExpansion)) {
|
|
149
|
+
return {
|
|
150
|
+
source: 'sidecar-manual',
|
|
151
|
+
mode: Number.isInteger(sidecar.mode) ? sidecar.mode : null,
|
|
152
|
+
expansionMil: manualExpansion,
|
|
153
|
+
unit: 'mil'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Returns a primitive-local mask value when present.
|
|
162
|
+
* @param {object} primitive Primitive row.
|
|
163
|
+
* @param {'paste' | 'solder'} side Mask side.
|
|
164
|
+
* @returns {object | null}
|
|
165
|
+
*/
|
|
166
|
+
static #primitiveValue(primitive, side) {
|
|
167
|
+
const prefix = side === 'paste' ? 'paste' : 'solder'
|
|
168
|
+
const mode = PcbMaskPasteResolver.#numberOrNull(
|
|
169
|
+
primitive?.[prefix + 'MaskExpansionMode']
|
|
170
|
+
)
|
|
171
|
+
const rawExpansion = PcbMaskPasteResolver.#numberOrNull(
|
|
172
|
+
primitive?.[prefix + 'MaskExpansion']
|
|
173
|
+
)
|
|
174
|
+
const effectiveExpansion = PcbMaskPasteResolver.#numberOrNull(
|
|
175
|
+
primitive?.[
|
|
176
|
+
'effective' +
|
|
177
|
+
PcbMaskPasteResolver.#title(prefix) +
|
|
178
|
+
'MaskExpansion'
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if (mode === 1) {
|
|
183
|
+
return {
|
|
184
|
+
source: 'rule',
|
|
185
|
+
mode,
|
|
186
|
+
expansionMil: effectiveExpansion ?? rawExpansion,
|
|
187
|
+
unit: 'mil'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (mode === 2) {
|
|
191
|
+
return {
|
|
192
|
+
source: 'primitive-manual',
|
|
193
|
+
mode,
|
|
194
|
+
expansionMil: effectiveExpansion ?? rawExpansion,
|
|
195
|
+
unit: 'mil'
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the first matching rule-derived expansion.
|
|
204
|
+
* @param {object[]} rules Parsed rules.
|
|
205
|
+
* @param {'paste' | 'solder'} side Mask side.
|
|
206
|
+
* @returns {object | null}
|
|
207
|
+
*/
|
|
208
|
+
static #ruleValue(rules, side) {
|
|
209
|
+
const rule = PcbMaskPasteResolver.#maskRules(rules).find(
|
|
210
|
+
(candidate) => candidate.side === side
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if (!rule || rule.expansionMil === null) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
source: 'rule',
|
|
219
|
+
ruleName: rule.name,
|
|
220
|
+
ruleKind: rule.ruleKind,
|
|
221
|
+
expansionMil: rule.expansionMil,
|
|
222
|
+
unit: 'mil'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extracts mask/paste rule summaries.
|
|
228
|
+
* @param {object[]} rules Parsed rules.
|
|
229
|
+
* @returns {object[]}
|
|
230
|
+
*/
|
|
231
|
+
static #maskRules(rules) {
|
|
232
|
+
return (rules || [])
|
|
233
|
+
.map((rule) => PcbMaskPasteResolver.#maskRule(rule))
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extracts one mask/paste rule summary.
|
|
239
|
+
* @param {object} rule Parsed rule.
|
|
240
|
+
* @returns {object | null}
|
|
241
|
+
*/
|
|
242
|
+
static #maskRule(rule) {
|
|
243
|
+
const token = [
|
|
244
|
+
rule?.ruleKind,
|
|
245
|
+
rule?.ruleType?.kind,
|
|
246
|
+
rule?.ruleType?.displayName,
|
|
247
|
+
rule?.name
|
|
248
|
+
]
|
|
249
|
+
.join(' ')
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
const side = token.includes('paste')
|
|
252
|
+
? 'paste'
|
|
253
|
+
: token.includes('solder') || token.includes('mask')
|
|
254
|
+
? 'solder'
|
|
255
|
+
: ''
|
|
256
|
+
|
|
257
|
+
if (!side) {
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
name: rule.name || '',
|
|
263
|
+
ruleKind: rule.ruleKind || '',
|
|
264
|
+
side,
|
|
265
|
+
expansionMil: PcbMaskPasteResolver.#firstConstraintMil(rule) ?? null
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Returns the first length-valued constraint in mils.
|
|
271
|
+
* @param {object} rule Parsed rule.
|
|
272
|
+
* @returns {number | null}
|
|
273
|
+
*/
|
|
274
|
+
static #firstConstraintMil(rule) {
|
|
275
|
+
for (const value of Object.values(rule?.constraintValues || {})) {
|
|
276
|
+
if (Number.isFinite(value?.valueMil)) return value.valueMil
|
|
277
|
+
if (
|
|
278
|
+
value?.type === 'number' &&
|
|
279
|
+
Number.isFinite(Number(value.value))
|
|
280
|
+
) {
|
|
281
|
+
return Number(value.value)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns the document default expansion for one side.
|
|
290
|
+
* @param {object | null} defaults Board defaults.
|
|
291
|
+
* @param {'paste' | 'solder'} side Mask side.
|
|
292
|
+
* @returns {number | null}
|
|
293
|
+
*/
|
|
294
|
+
static #defaultExpansion(defaults, side) {
|
|
295
|
+
const expansion = Number(defaults?.maskPaste?.[side]?.expansionMil)
|
|
296
|
+
return Number.isFinite(expansion) ? expansion : null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Builds summary counts from resolved primitive rows.
|
|
301
|
+
* @param {object[]} primitiveRows Resolved primitive rows.
|
|
302
|
+
* @returns {object}
|
|
303
|
+
*/
|
|
304
|
+
static #summarize(primitiveRows) {
|
|
305
|
+
const summary = {
|
|
306
|
+
primitiveCount: primitiveRows.length,
|
|
307
|
+
manualCount: 0,
|
|
308
|
+
ruleCount: 0,
|
|
309
|
+
defaultCount: 0,
|
|
310
|
+
unresolvedCount: 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const row of primitiveRows) {
|
|
314
|
+
for (const side of ['paste', 'solder']) {
|
|
315
|
+
const source = row[side]?.source || 'unresolved'
|
|
316
|
+
if (
|
|
317
|
+
source === 'sidecar-manual' ||
|
|
318
|
+
source === 'primitive-manual'
|
|
319
|
+
) {
|
|
320
|
+
summary.manualCount += 1
|
|
321
|
+
} else if (source === 'rule') {
|
|
322
|
+
summary.ruleCount += 1
|
|
323
|
+
} else if (source === 'document-default') {
|
|
324
|
+
summary.defaultCount += 1
|
|
325
|
+
} else {
|
|
326
|
+
summary.unresolvedCount += 1
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return summary
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Converts a value to a finite number or null.
|
|
336
|
+
* @param {unknown} value Source value.
|
|
337
|
+
* @returns {number | null}
|
|
338
|
+
*/
|
|
339
|
+
static #numberOrNull(value) {
|
|
340
|
+
const number = Number(value)
|
|
341
|
+
return Number.isFinite(number) ? number : null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Title-cases one ASCII token.
|
|
346
|
+
* @param {string} value Token.
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
static #title(value) {
|
|
350
|
+
return String(value || '').replace(/^./u, (letter) =>
|
|
351
|
+
letter.toUpperCase()
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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 mechanical-layer pair metadata used when footprints move between
|
|
9
|
+
* board sides.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbMechanicalLayerPairParser {
|
|
12
|
+
/**
|
|
13
|
+
* Parses mechanical-layer pairs from board metadata records.
|
|
14
|
+
* @param {Record<string, string | string[]>[]} fieldSets Board field sets.
|
|
15
|
+
* @param {{ layerId: number | null, name: string }[]} layers Layer stack rows.
|
|
16
|
+
* @param {{ layerId: number, name: string }[]} primitiveLayers Primitive layer rows.
|
|
17
|
+
* @returns {{ index: number, layer1Id: number, layer2Id: number, layer1Name: string, layer2Name: string, layer1V7SaveId?: number, layer2V7SaveId?: number }[]}
|
|
18
|
+
*/
|
|
19
|
+
static parse(fieldSets, layers = [], primitiveLayers = []) {
|
|
20
|
+
const layerNames = PcbMechanicalLayerPairParser.#buildLayerNameMap(
|
|
21
|
+
fieldSets,
|
|
22
|
+
layers,
|
|
23
|
+
primitiveLayers
|
|
24
|
+
)
|
|
25
|
+
const pairs = []
|
|
26
|
+
|
|
27
|
+
for (const fields of fieldSets || []) {
|
|
28
|
+
for (const index of PcbMechanicalLayerPairParser.#pairIndexes(
|
|
29
|
+
fields
|
|
30
|
+
)) {
|
|
31
|
+
const layer1Id = PcbMechanicalLayerPairParser.#numberField(
|
|
32
|
+
fields,
|
|
33
|
+
index,
|
|
34
|
+
'layer1'
|
|
35
|
+
)
|
|
36
|
+
const layer2Id = PcbMechanicalLayerPairParser.#numberField(
|
|
37
|
+
fields,
|
|
38
|
+
index,
|
|
39
|
+
'layer2'
|
|
40
|
+
)
|
|
41
|
+
if (
|
|
42
|
+
!Number.isInteger(layer1Id) ||
|
|
43
|
+
!Number.isInteger(layer2Id)
|
|
44
|
+
) {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pairs.push({
|
|
49
|
+
index,
|
|
50
|
+
layer1Id,
|
|
51
|
+
layer2Id,
|
|
52
|
+
layer1Name: layerNames.get(layer1Id) || 'Layer ' + layer1Id,
|
|
53
|
+
layer2Name: layerNames.get(layer2Id) || 'Layer ' + layer2Id,
|
|
54
|
+
...PcbMechanicalLayerPairParser.#v7Fields(fields, index)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return PcbMechanicalLayerPairParser.#dedupePairs(pairs)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds layer-flip metadata from parsed mechanical pairs.
|
|
64
|
+
* @param {{ layer1Id: number, layer2Id: number }[]} pairs Mechanical pairs.
|
|
65
|
+
* @returns {{ mechanicalFlipMap: Record<string, number>, pairedLayerIds: number[] }}
|
|
66
|
+
*/
|
|
67
|
+
static buildFlipMetadata(pairs) {
|
|
68
|
+
const mechanicalFlipMap = {}
|
|
69
|
+
for (const pair of pairs || []) {
|
|
70
|
+
mechanicalFlipMap[String(pair.layer1Id)] = pair.layer2Id
|
|
71
|
+
mechanicalFlipMap[String(pair.layer2Id)] = pair.layer1Id
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
mechanicalFlipMap,
|
|
76
|
+
pairedLayerIds: Object.keys(mechanicalFlipMap)
|
|
77
|
+
.map((layerId) => Number.parseInt(layerId, 10))
|
|
78
|
+
.sort((left, right) => left - right)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds a layer-id to display-name map.
|
|
84
|
+
* @param {Record<string, string | string[]>[]} fieldSets Board field sets.
|
|
85
|
+
* @param {{ layerId: number | null, name: string }[]} layers Layer stack rows.
|
|
86
|
+
* @param {{ layerId: number, name: string }[]} primitiveLayers Primitive layer rows.
|
|
87
|
+
* @returns {Map<number, string>}
|
|
88
|
+
*/
|
|
89
|
+
static #buildLayerNameMap(fieldSets, layers, primitiveLayers) {
|
|
90
|
+
const names = new Map()
|
|
91
|
+
|
|
92
|
+
for (const layer of [...(layers || []), ...(primitiveLayers || [])]) {
|
|
93
|
+
const layerId = Number(layer.layerId)
|
|
94
|
+
if (Number.isInteger(layerId) && layer.name) {
|
|
95
|
+
names.set(layerId, layer.name)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const fields of fieldSets || []) {
|
|
100
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
101
|
+
const match = String(key).match(/^LAYER(\d+)NAME$/iu)
|
|
102
|
+
if (!match) continue
|
|
103
|
+
names.set(Number.parseInt(match[1], 10), String(value || ''))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return names
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Finds mechanical pair indexes declared in one field set.
|
|
112
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
113
|
+
* @returns {number[]}
|
|
114
|
+
*/
|
|
115
|
+
static #pairIndexes(fields) {
|
|
116
|
+
const indexes = new Set()
|
|
117
|
+
const declaredCount =
|
|
118
|
+
ParserUtils.parseNumericField(
|
|
119
|
+
fields,
|
|
120
|
+
'MECHANICAL_LAYER_PAIR_COUNT'
|
|
121
|
+
) ||
|
|
122
|
+
ParserUtils.parseNumericField(fields, 'MECHANICALPAIRCOUNT') ||
|
|
123
|
+
0
|
|
124
|
+
|
|
125
|
+
for (let index = 1; index <= declaredCount; index += 1) {
|
|
126
|
+
indexes.add(index)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const key of Object.keys(fields || {})) {
|
|
130
|
+
const match = String(key).match(/^MECHANICAL_?LAYER_?PAIR(\d+)_?/iu)
|
|
131
|
+
if (match) indexes.add(Number.parseInt(match[1], 10))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return [...indexes].sort((left, right) => left - right)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reads one layer id from supported mechanical pair key variants.
|
|
139
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
140
|
+
* @param {number} index Pair index.
|
|
141
|
+
* @param {'layer1' | 'layer2'} role Pair side.
|
|
142
|
+
* @returns {number | null}
|
|
143
|
+
*/
|
|
144
|
+
static #numberField(fields, index, role) {
|
|
145
|
+
const keys =
|
|
146
|
+
role === 'layer1'
|
|
147
|
+
? [
|
|
148
|
+
`MECHANICAL_LAYER_PAIR${index}_LAYER1`,
|
|
149
|
+
`MECHANICAL_LAYER_PAIR${index}_FIRST`,
|
|
150
|
+
`MECHANICALLAYERPAIR${index}LAYER1`,
|
|
151
|
+
`MECHPAIR${index}LAYER1`
|
|
152
|
+
]
|
|
153
|
+
: [
|
|
154
|
+
`MECHANICAL_LAYER_PAIR${index}_LAYER2`,
|
|
155
|
+
`MECHANICAL_LAYER_PAIR${index}_SECOND`,
|
|
156
|
+
`MECHANICALLAYERPAIR${index}LAYER2`,
|
|
157
|
+
`MECHPAIR${index}LAYER2`
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
const value = ParserUtils.parseNumericField(fields, key)
|
|
162
|
+
if (Number.isInteger(value)) return value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reads optional V7 saved-layer ids for one pair.
|
|
170
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
171
|
+
* @param {number} index Pair index.
|
|
172
|
+
* @returns {{ layer1V7SaveId?: number, layer2V7SaveId?: number }}
|
|
173
|
+
*/
|
|
174
|
+
static #v7Fields(fields, index) {
|
|
175
|
+
const layer1V7SaveId = ParserUtils.parseNumericField(
|
|
176
|
+
fields,
|
|
177
|
+
`MECHANICAL_LAYER_PAIR${index}_LAYER1V7`
|
|
178
|
+
)
|
|
179
|
+
const layer2V7SaveId = ParserUtils.parseNumericField(
|
|
180
|
+
fields,
|
|
181
|
+
`MECHANICAL_LAYER_PAIR${index}_LAYER2V7`
|
|
182
|
+
)
|
|
183
|
+
return {
|
|
184
|
+
...(Number.isInteger(layer1V7SaveId) ? { layer1V7SaveId } : {}),
|
|
185
|
+
...(Number.isInteger(layer2V7SaveId) ? { layer2V7SaveId } : {})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Removes duplicate pairs while preserving first-seen metadata.
|
|
191
|
+
* @param {object[]} pairs Candidate pairs.
|
|
192
|
+
* @returns {object[]}
|
|
193
|
+
*/
|
|
194
|
+
static #dedupePairs(pairs) {
|
|
195
|
+
const byKey = new Map()
|
|
196
|
+
for (const pair of pairs || []) {
|
|
197
|
+
const key = [pair.layer1Id, pair.layer2Id].sort().join(':')
|
|
198
|
+
if (!byKey.has(key)) {
|
|
199
|
+
byKey.set(key, pair)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [...byKey.values()]
|
|
203
|
+
}
|
|
204
|
+
}
|