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,338 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds read-only extraction manifests for placed PCB footprints.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPlacedFootprintManifestBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.pcb.placed-footprint-extraction.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a placed-footprint extraction manifest.
|
|
13
|
+
* @param {{ fileName?: string, components?: object[], componentPrimitiveGroups?: object[], embeddedModels?: object[] }} context Manifest context.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(context = {}) {
|
|
17
|
+
const outputs = (context.componentPrimitiveGroups || []).map(
|
|
18
|
+
(group, index) =>
|
|
19
|
+
PcbPlacedFootprintManifestBuilder.#output(context, group, index)
|
|
20
|
+
)
|
|
21
|
+
const embeddedAssetCount = outputs.reduce(
|
|
22
|
+
(total, output) => total + output.embeddedAssets.length,
|
|
23
|
+
0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
schema: PcbPlacedFootprintManifestBuilder.SCHEMA,
|
|
28
|
+
sourceDocument: String(context.fileName || ''),
|
|
29
|
+
summary: {
|
|
30
|
+
componentCount: (context.components || []).length,
|
|
31
|
+
extractableFootprintCount: outputs.length,
|
|
32
|
+
embeddedAssetCount
|
|
33
|
+
},
|
|
34
|
+
outputs,
|
|
35
|
+
indexes: PcbPlacedFootprintManifestBuilder.#indexes(outputs)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds one placed-footprint output descriptor.
|
|
41
|
+
* @param {object} context Manifest context.
|
|
42
|
+
* @param {object} group Component primitive group.
|
|
43
|
+
* @param {number} index Group index.
|
|
44
|
+
* @returns {object}
|
|
45
|
+
*/
|
|
46
|
+
static #output(context, group, index) {
|
|
47
|
+
const component =
|
|
48
|
+
(context.components || []).find(
|
|
49
|
+
(candidate) =>
|
|
50
|
+
Number(candidate.componentIndex) ===
|
|
51
|
+
Number(group.componentIndex)
|
|
52
|
+
) || {}
|
|
53
|
+
const designator = group.designator || component.designator || ''
|
|
54
|
+
const pattern = component.pattern || ''
|
|
55
|
+
const footprintKey =
|
|
56
|
+
'footprint-extract-' +
|
|
57
|
+
index +
|
|
58
|
+
'-' +
|
|
59
|
+
PcbPlacedFootprintManifestBuilder.#slug(
|
|
60
|
+
[designator, pattern].filter(Boolean).join('-') || index
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
kind: 'placed-footprint',
|
|
65
|
+
footprintKey,
|
|
66
|
+
designator,
|
|
67
|
+
pattern,
|
|
68
|
+
componentIndex: Number(group.componentIndex),
|
|
69
|
+
outputLibraryKey: 'pcb-extract/' + footprintKey + '.PcbLib',
|
|
70
|
+
renderManifestKey: 'pcb-extract/' + footprintKey + '.render.json',
|
|
71
|
+
primitiveCounts:
|
|
72
|
+
PcbPlacedFootprintManifestBuilder.#primitiveCounts(group),
|
|
73
|
+
layers: PcbPlacedFootprintManifestBuilder.#layers(group),
|
|
74
|
+
embeddedAssets: PcbPlacedFootprintManifestBuilder.#embeddedAssets(
|
|
75
|
+
group,
|
|
76
|
+
context.embeddedModels || []
|
|
77
|
+
),
|
|
78
|
+
diagnostics: PcbPlacedFootprintManifestBuilder.#diagnostics(group)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Counts footprint-owned primitive families.
|
|
84
|
+
* @param {object} group Component primitive group.
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
static #primitiveCounts(group) {
|
|
88
|
+
return {
|
|
89
|
+
pads: (group.pads || []).length,
|
|
90
|
+
tracks: (group.tracks || []).length,
|
|
91
|
+
arcs: (group.arcs || []).length,
|
|
92
|
+
fills: (group.fills || []).length,
|
|
93
|
+
vias: (group.vias || []).length,
|
|
94
|
+
regions: (group.regions || []).length,
|
|
95
|
+
shapeBasedRegions: (group.shapeBasedRegions || []).length,
|
|
96
|
+
texts: (group.texts || []).length,
|
|
97
|
+
componentBodies: (group.componentBodies || []).length
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds layer descriptors touched by one footprint.
|
|
103
|
+
* @param {object} group Component primitive group.
|
|
104
|
+
* @returns {object[]}
|
|
105
|
+
*/
|
|
106
|
+
static #layers(group) {
|
|
107
|
+
const layerMap = new Map()
|
|
108
|
+
for (const primitive of PcbPlacedFootprintManifestBuilder.#primitives(
|
|
109
|
+
group
|
|
110
|
+
)) {
|
|
111
|
+
const layer =
|
|
112
|
+
PcbPlacedFootprintManifestBuilder.#layerDescriptor(primitive)
|
|
113
|
+
if (layer) {
|
|
114
|
+
layerMap.set(layer.layerKey, layer)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...layerMap.values()].sort((left, right) =>
|
|
118
|
+
left.layerKey.localeCompare(right.layerKey, undefined, {
|
|
119
|
+
numeric: true
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Collects embedded assets referenced by component bodies.
|
|
126
|
+
* @param {object} group Component primitive group.
|
|
127
|
+
* @param {object[]} embeddedModels Embedded model rows.
|
|
128
|
+
* @returns {object[]}
|
|
129
|
+
*/
|
|
130
|
+
static #embeddedAssets(group, embeddedModels) {
|
|
131
|
+
return PcbPlacedFootprintManifestBuilder.#dedupe(
|
|
132
|
+
(group.componentBodies || []).flatMap((body) => {
|
|
133
|
+
const model =
|
|
134
|
+
PcbPlacedFootprintManifestBuilder.#matchingModel(
|
|
135
|
+
body,
|
|
136
|
+
embeddedModels
|
|
137
|
+
) || body
|
|
138
|
+
return [
|
|
139
|
+
PcbPlacedFootprintManifestBuilder.#stripUndefined({
|
|
140
|
+
key: model.id || body.modelId || model.name,
|
|
141
|
+
format: model.format,
|
|
142
|
+
sourceStream: model.sourceStream,
|
|
143
|
+
name: model.name || body.name
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds extraction diagnostics for one group.
|
|
152
|
+
* @param {object} group Component primitive group.
|
|
153
|
+
* @returns {object[]}
|
|
154
|
+
*/
|
|
155
|
+
static #diagnostics(group) {
|
|
156
|
+
const diagnostics = []
|
|
157
|
+
if (!PcbPlacedFootprintManifestBuilder.#hasOwnedGeometry(group)) {
|
|
158
|
+
diagnostics.push({
|
|
159
|
+
code: 'pcb-footprint-extract.empty-geometry',
|
|
160
|
+
severity: 'warning',
|
|
161
|
+
message: 'Placed component has no owned footprint geometry.'
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
return diagnostics
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Builds manifest lookup indexes.
|
|
169
|
+
* @param {object[]} outputs Output descriptors.
|
|
170
|
+
* @returns {object}
|
|
171
|
+
*/
|
|
172
|
+
static #indexes(outputs) {
|
|
173
|
+
const outputsByDesignator = {}
|
|
174
|
+
const outputsByPattern = {}
|
|
175
|
+
|
|
176
|
+
outputs.forEach((output, index) => {
|
|
177
|
+
if (output.designator)
|
|
178
|
+
outputsByDesignator[output.designator] = index
|
|
179
|
+
if (output.pattern) {
|
|
180
|
+
outputsByPattern[output.pattern] =
|
|
181
|
+
outputsByPattern[output.pattern] || []
|
|
182
|
+
outputsByPattern[output.pattern].push(index)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
outputsByDesignator,
|
|
188
|
+
outputsByPattern
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns all geometry primitives from a group.
|
|
194
|
+
* @param {object} group Component primitive group.
|
|
195
|
+
* @returns {object[]}
|
|
196
|
+
*/
|
|
197
|
+
static #primitives(group) {
|
|
198
|
+
return [
|
|
199
|
+
...(group.pads || []),
|
|
200
|
+
...(group.tracks || []),
|
|
201
|
+
...(group.arcs || []),
|
|
202
|
+
...(group.fills || []),
|
|
203
|
+
...(group.vias || []),
|
|
204
|
+
...(group.regions || []),
|
|
205
|
+
...(group.shapeBasedRegions || []),
|
|
206
|
+
...(group.texts || [])
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns true when a component has any extractable geometry.
|
|
212
|
+
* @param {object} group Component primitive group.
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
static #hasOwnedGeometry(group) {
|
|
216
|
+
return (
|
|
217
|
+
PcbPlacedFootprintManifestBuilder.#primitives(group).length > 0 ||
|
|
218
|
+
(group.componentBodies || []).length > 0
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Builds a normalized layer descriptor.
|
|
224
|
+
* @param {object} primitive Primitive row.
|
|
225
|
+
* @returns {object | null}
|
|
226
|
+
*/
|
|
227
|
+
static #layerDescriptor(primitive) {
|
|
228
|
+
const layerId = Number.isInteger(primitive?.layerId)
|
|
229
|
+
? primitive.layerId
|
|
230
|
+
: null
|
|
231
|
+
const displayName = String(
|
|
232
|
+
primitive?.layerName || primitive?.layer || ''
|
|
233
|
+
).trim()
|
|
234
|
+
if (layerId === null && !displayName) {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
const layerKey =
|
|
238
|
+
layerId === null
|
|
239
|
+
? 'layer-' +
|
|
240
|
+
PcbPlacedFootprintManifestBuilder.#slug(displayName)
|
|
241
|
+
: 'L' + layerId
|
|
242
|
+
|
|
243
|
+
return PcbPlacedFootprintManifestBuilder.#stripUndefined({
|
|
244
|
+
layerKey,
|
|
245
|
+
layerId: layerId === null ? undefined : layerId,
|
|
246
|
+
displayName: displayName || layerKey
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resolves an embedded model row for one component body.
|
|
252
|
+
* @param {object} body Component body row.
|
|
253
|
+
* @param {object[]} embeddedModels Embedded model rows.
|
|
254
|
+
* @returns {object | null}
|
|
255
|
+
*/
|
|
256
|
+
static #matchingModel(body, embeddedModels) {
|
|
257
|
+
return (
|
|
258
|
+
(embeddedModels || []).find(
|
|
259
|
+
(model) =>
|
|
260
|
+
PcbPlacedFootprintManifestBuilder.#same(
|
|
261
|
+
model.id,
|
|
262
|
+
body.modelId
|
|
263
|
+
) ||
|
|
264
|
+
PcbPlacedFootprintManifestBuilder.#same(
|
|
265
|
+
model.checksum,
|
|
266
|
+
body.checksum
|
|
267
|
+
) ||
|
|
268
|
+
PcbPlacedFootprintManifestBuilder.#same(
|
|
269
|
+
model.name,
|
|
270
|
+
body.name
|
|
271
|
+
)
|
|
272
|
+
) || null
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Compares two non-empty values.
|
|
278
|
+
* @param {unknown} left First value.
|
|
279
|
+
* @param {unknown} right Second value.
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
static #same(left, right) {
|
|
283
|
+
return (
|
|
284
|
+
left !== null &&
|
|
285
|
+
left !== undefined &&
|
|
286
|
+
left !== '' &&
|
|
287
|
+
right !== null &&
|
|
288
|
+
right !== undefined &&
|
|
289
|
+
right !== '' &&
|
|
290
|
+
String(left) === String(right)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Deduplicates objects by their JSON form.
|
|
296
|
+
* @param {object[]} rows Candidate rows.
|
|
297
|
+
* @returns {object[]}
|
|
298
|
+
*/
|
|
299
|
+
static #dedupe(rows) {
|
|
300
|
+
const seen = new Set()
|
|
301
|
+
const deduped = []
|
|
302
|
+
for (const row of rows || []) {
|
|
303
|
+
const key = JSON.stringify(row)
|
|
304
|
+
if (seen.has(key)) continue
|
|
305
|
+
seen.add(key)
|
|
306
|
+
deduped.push(row)
|
|
307
|
+
}
|
|
308
|
+
return deduped
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Converts a value to a deterministic lowercase key segment.
|
|
313
|
+
* @param {unknown} value Source value.
|
|
314
|
+
* @returns {string}
|
|
315
|
+
*/
|
|
316
|
+
static #slug(value) {
|
|
317
|
+
return (
|
|
318
|
+
String(value || '')
|
|
319
|
+
.trim()
|
|
320
|
+
.toLowerCase()
|
|
321
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
322
|
+
.replace(/^-+|-+$/gu, '') || 'item'
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Removes undefined fields from one object.
|
|
328
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
329
|
+
* @returns {Record<string, unknown>}
|
|
330
|
+
*/
|
|
331
|
+
static #stripUndefined(value) {
|
|
332
|
+
return Object.fromEntries(
|
|
333
|
+
Object.entries(value || {}).filter(
|
|
334
|
+
([, entryValue]) => entryValue !== undefined
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
|
|
6
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
|
+
|
|
8
|
+
const { getField, parseBoolean, parseNumericField } = ParserUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes printable PCB polygon records into renderable polygon rows.
|
|
12
|
+
*/
|
|
13
|
+
export class PcbPolygonRecordParser {
|
|
14
|
+
/**
|
|
15
|
+
* Parses polygon rows from printable records.
|
|
16
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records Printable records.
|
|
17
|
+
* @returns {object[]}
|
|
18
|
+
*/
|
|
19
|
+
static parse(records) {
|
|
20
|
+
return (records || [])
|
|
21
|
+
.filter(
|
|
22
|
+
(record) =>
|
|
23
|
+
record.sourceStream === 'Polygons6/Data' &&
|
|
24
|
+
getField(record.fields, 'KIND0')
|
|
25
|
+
)
|
|
26
|
+
.map((record, index) =>
|
|
27
|
+
PcbPolygonRecordParser.#normalize(record.fields, index)
|
|
28
|
+
)
|
|
29
|
+
.filter((polygon) => polygon.segments.length > 0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes one printable polygon row.
|
|
34
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
35
|
+
* @param {number} index Fallback row index.
|
|
36
|
+
* @returns {object}
|
|
37
|
+
*/
|
|
38
|
+
static #normalize(fields, index) {
|
|
39
|
+
const outline = AltiumLayoutParser.parseBoardOutline(fields)
|
|
40
|
+
return PcbPolygonRecordParser.#stripEmpty({
|
|
41
|
+
layer: getField(fields, 'LAYER') || 'UNKNOWN',
|
|
42
|
+
polygonIndex: PcbPolygonRecordParser.#firstNumber(fields, [
|
|
43
|
+
'POLYGONINDEX',
|
|
44
|
+
'POLYGON_INDEX',
|
|
45
|
+
'INDEX'
|
|
46
|
+
]),
|
|
47
|
+
subpolygonIndex: PcbPolygonRecordParser.#firstNumber(fields, [
|
|
48
|
+
'SUBPOLYINDEX',
|
|
49
|
+
'SUBPOLYGONINDEX',
|
|
50
|
+
'SUB_POLYGON_INDEX'
|
|
51
|
+
]),
|
|
52
|
+
unionIndex: PcbPolygonRecordParser.#firstNumber(fields, [
|
|
53
|
+
'UNIONINDEX',
|
|
54
|
+
'UNION_INDEX'
|
|
55
|
+
]),
|
|
56
|
+
isCutout: PcbPolygonRecordParser.#firstBoolean(fields, [
|
|
57
|
+
'ISCUTOUT',
|
|
58
|
+
'IS_CUTOUT',
|
|
59
|
+
'CUTOUT'
|
|
60
|
+
]),
|
|
61
|
+
realizationKind:
|
|
62
|
+
getField(fields, 'POLYGONKIND') ||
|
|
63
|
+
getField(fields, 'POURKIND') ||
|
|
64
|
+
'',
|
|
65
|
+
sourceRecordIndex: index,
|
|
66
|
+
segments: outline.segments
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads the first numeric field from a list of native aliases.
|
|
72
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
73
|
+
* @param {string[]} keys Candidate keys.
|
|
74
|
+
* @returns {number | undefined}
|
|
75
|
+
*/
|
|
76
|
+
static #firstNumber(fields, keys) {
|
|
77
|
+
for (const key of keys) {
|
|
78
|
+
const value = parseNumericField(fields, key)
|
|
79
|
+
if (value !== null) {
|
|
80
|
+
return value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads the first boolean field from a list of native aliases.
|
|
89
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
90
|
+
* @param {string[]} keys Candidate keys.
|
|
91
|
+
* @returns {boolean | undefined}
|
|
92
|
+
*/
|
|
93
|
+
static #firstBoolean(fields, keys) {
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (getField(fields, key)) {
|
|
96
|
+
return parseBoolean(fields[key])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Removes empty optional fields while preserving zero and false.
|
|
105
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
106
|
+
* @returns {Record<string, unknown>}
|
|
107
|
+
*/
|
|
108
|
+
static #stripEmpty(value) {
|
|
109
|
+
return Object.fromEntries(
|
|
110
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
111
|
+
if (Array.isArray(entryValue)) return entryValue.length > 0
|
|
112
|
+
return (
|
|
113
|
+
entryValue !== null &&
|
|
114
|
+
entryValue !== undefined &&
|
|
115
|
+
entryValue !== ''
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|