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,532 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic PCB QA and statistics summaries.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbStatisticsBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Builds statistics for one normalized PCB object.
|
|
11
|
+
* @param {object} pcb Normalized PCB model.
|
|
12
|
+
* @returns {object}
|
|
13
|
+
*/
|
|
14
|
+
static build(pcb) {
|
|
15
|
+
return {
|
|
16
|
+
schema: 'altium-toolkit.pcb.statistics.a1',
|
|
17
|
+
board: PcbStatisticsBuilder.#boardStats(pcb?.boardOutline || {}),
|
|
18
|
+
drills: PcbStatisticsBuilder.#drillStats(
|
|
19
|
+
pcb?.pads || [],
|
|
20
|
+
pcb?.vias || []
|
|
21
|
+
),
|
|
22
|
+
primitiveWidths: PcbStatisticsBuilder.#primitiveWidthStats(pcb),
|
|
23
|
+
layers: PcbStatisticsBuilder.#layerStats(pcb),
|
|
24
|
+
planning: PcbStatisticsBuilder.#planningStats(pcb)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds board outline statistics.
|
|
30
|
+
* @param {object} boardOutline Board outline object.
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
static #boardStats(boardOutline) {
|
|
34
|
+
const widthMil = PcbStatisticsBuilder.#round(boardOutline.widthMil)
|
|
35
|
+
const heightMil = PcbStatisticsBuilder.#round(boardOutline.heightMil)
|
|
36
|
+
const minX = Number(boardOutline.minX || 0)
|
|
37
|
+
const minY = Number(boardOutline.minY || 0)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
widthMil,
|
|
41
|
+
heightMil,
|
|
42
|
+
centroidMil: {
|
|
43
|
+
x: PcbStatisticsBuilder.#round(minX + widthMil / 2),
|
|
44
|
+
y: PcbStatisticsBuilder.#round(minY + heightMil / 2)
|
|
45
|
+
},
|
|
46
|
+
outlineSegmentCount: Array.isArray(boardOutline.segments)
|
|
47
|
+
? boardOutline.segments.length
|
|
48
|
+
: 0,
|
|
49
|
+
cutoutCount: Array.isArray(boardOutline.cutouts)
|
|
50
|
+
? boardOutline.cutouts.length
|
|
51
|
+
: 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds drill and slot counters.
|
|
57
|
+
* @param {object[]} pads Pad primitives.
|
|
58
|
+
* @param {object[]} vias Via primitives.
|
|
59
|
+
* @returns {object}
|
|
60
|
+
*/
|
|
61
|
+
static #drillStats(pads, vias) {
|
|
62
|
+
const padHoles = (pads || []).filter((pad) =>
|
|
63
|
+
PcbStatisticsBuilder.#hasHole(pad)
|
|
64
|
+
)
|
|
65
|
+
const viaHoles = (vias || []).filter((via) =>
|
|
66
|
+
PcbStatisticsBuilder.#hasHole(via)
|
|
67
|
+
)
|
|
68
|
+
const holes = [...padHoles, ...viaHoles]
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
totalHoleCount: holes.length,
|
|
72
|
+
padHoleCount: padHoles.length,
|
|
73
|
+
viaHoleCount: viaHoles.length,
|
|
74
|
+
platedHoleCount: holes.filter((hole) => hole.isPlated !== false)
|
|
75
|
+
.length,
|
|
76
|
+
nonPlatedHoleCount: holes.filter((hole) => hole.isPlated === false)
|
|
77
|
+
.length,
|
|
78
|
+
slotCount: holes.filter((hole) =>
|
|
79
|
+
PcbStatisticsBuilder.#hasSlot(hole)
|
|
80
|
+
).length,
|
|
81
|
+
holeDiameterMil: PcbStatisticsBuilder.#histogram(
|
|
82
|
+
holes.map((hole) => hole.holeDiameter)
|
|
83
|
+
),
|
|
84
|
+
slotLengthMil: PcbStatisticsBuilder.#histogram(
|
|
85
|
+
holes
|
|
86
|
+
.filter((hole) => PcbStatisticsBuilder.#hasSlot(hole))
|
|
87
|
+
.map((hole) => hole.holeSlotLength || hole.slotLength)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds primitive-width histograms.
|
|
94
|
+
* @param {object} pcb Normalized PCB model.
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
static #primitiveWidthStats(pcb) {
|
|
98
|
+
return {
|
|
99
|
+
tracksMil: PcbStatisticsBuilder.#histogram(
|
|
100
|
+
(pcb?.tracks || []).map((track) => track.width)
|
|
101
|
+
),
|
|
102
|
+
arcsMil: PcbStatisticsBuilder.#histogram(
|
|
103
|
+
(pcb?.arcs || []).map((arc) => arc.width)
|
|
104
|
+
),
|
|
105
|
+
viasMil: PcbStatisticsBuilder.#histogram(
|
|
106
|
+
(pcb?.vias || []).map((via) => via.diameter)
|
|
107
|
+
),
|
|
108
|
+
padsTopXMil: PcbStatisticsBuilder.#histogram(
|
|
109
|
+
(pcb?.pads || []).map((pad) => pad.sizeTopX)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Builds a layer-stack and per-layer primitive summary.
|
|
116
|
+
* @param {object} pcb Normalized PCB model.
|
|
117
|
+
* @returns {object}
|
|
118
|
+
*/
|
|
119
|
+
static #layerStats(pcb) {
|
|
120
|
+
const layerIds = PcbStatisticsBuilder.#collectLayerIds(pcb)
|
|
121
|
+
const entries = layerIds.map((layerId) => {
|
|
122
|
+
const layer = PcbStatisticsBuilder.#findLayer(pcb, layerId)
|
|
123
|
+
|
|
124
|
+
return PcbStatisticsBuilder.#stripUndefined({
|
|
125
|
+
layerId,
|
|
126
|
+
name:
|
|
127
|
+
layer?.name ||
|
|
128
|
+
PcbStatisticsBuilder.#primitiveLayerName(pcb, layerId) ||
|
|
129
|
+
'L' + layerId,
|
|
130
|
+
role: PcbStatisticsBuilder.#layerRole(layer),
|
|
131
|
+
material: layer?.material,
|
|
132
|
+
thicknessMil: PcbStatisticsBuilder.#optionalRound(
|
|
133
|
+
layer?.thicknessMil
|
|
134
|
+
),
|
|
135
|
+
copperThicknessMil: PcbStatisticsBuilder.#optionalRound(
|
|
136
|
+
layer?.copperThicknessMil
|
|
137
|
+
),
|
|
138
|
+
copperWeight: layer?.copperWeight,
|
|
139
|
+
dielectricConstant: PcbStatisticsBuilder.#optionalRound(
|
|
140
|
+
layer?.dielectricConstant
|
|
141
|
+
),
|
|
142
|
+
dissipationFactor: PcbStatisticsBuilder.#optionalRound(
|
|
143
|
+
layer?.dissipationFactor
|
|
144
|
+
),
|
|
145
|
+
primitiveCounts: PcbStatisticsBuilder.#primitiveCountsForLayer(
|
|
146
|
+
pcb,
|
|
147
|
+
layerId
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
count: entries.length,
|
|
154
|
+
summary: PcbStatisticsBuilder.#layerMaterialSummary(
|
|
155
|
+
pcb?.layers || []
|
|
156
|
+
),
|
|
157
|
+
entries
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds board-planning statistics for keepouts, rooms, and rigid-flex regions.
|
|
163
|
+
* @param {object} pcb Normalized PCB model.
|
|
164
|
+
* @returns {{ keepouts: object, rooms: object, boardRegions: object }}
|
|
165
|
+
*/
|
|
166
|
+
static #planningStats(pcb) {
|
|
167
|
+
const regions = pcb?.regions || []
|
|
168
|
+
const shapeBasedRegions = pcb?.shapeBasedRegions || []
|
|
169
|
+
const boardRegions = pcb?.boardRegions || []
|
|
170
|
+
const roomNames = PcbStatisticsBuilder.#collectRoomNames(
|
|
171
|
+
pcb?.rules || []
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
keepouts: {
|
|
176
|
+
totalCount:
|
|
177
|
+
PcbStatisticsBuilder.#keepoutCount(regions) +
|
|
178
|
+
PcbStatisticsBuilder.#keepoutCount(shapeBasedRegions) +
|
|
179
|
+
PcbStatisticsBuilder.#keepoutCount(boardRegions),
|
|
180
|
+
regionCount: PcbStatisticsBuilder.#keepoutCount(regions),
|
|
181
|
+
shapeBasedRegionCount:
|
|
182
|
+
PcbStatisticsBuilder.#keepoutCount(shapeBasedRegions),
|
|
183
|
+
boardRegionCount:
|
|
184
|
+
PcbStatisticsBuilder.#keepoutCount(boardRegions)
|
|
185
|
+
},
|
|
186
|
+
rooms: {
|
|
187
|
+
ruleCount: PcbStatisticsBuilder.#roomRuleCount(
|
|
188
|
+
pcb?.rules || []
|
|
189
|
+
),
|
|
190
|
+
namedRoomCount: roomNames.length,
|
|
191
|
+
names: roomNames
|
|
192
|
+
},
|
|
193
|
+
boardRegions: {
|
|
194
|
+
boardRegionCount: boardRegions.length,
|
|
195
|
+
flexRegionCount: boardRegions.filter(
|
|
196
|
+
(region) => region?.isFlexRegion === true
|
|
197
|
+
).length,
|
|
198
|
+
rigidRegionCount: boardRegions.filter(
|
|
199
|
+
(region) => region?.isRigidRegion === true
|
|
200
|
+
).length,
|
|
201
|
+
locked3dCount: boardRegions.filter(
|
|
202
|
+
(region) => region?.locked3d === true
|
|
203
|
+
).length,
|
|
204
|
+
bendingLineCount: boardRegions.reduce(
|
|
205
|
+
(total, region) =>
|
|
206
|
+
total + Number(region?.bendingLineCount || 0),
|
|
207
|
+
0
|
|
208
|
+
),
|
|
209
|
+
layerStacks:
|
|
210
|
+
PcbStatisticsBuilder.#boardRegionLayerStacks(boardRegions)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Counts primitives marked as keepouts.
|
|
217
|
+
* @param {object[]} regions Region-like primitives.
|
|
218
|
+
* @returns {number}
|
|
219
|
+
*/
|
|
220
|
+
static #keepoutCount(regions) {
|
|
221
|
+
return (regions || []).filter((region) => region?.isKeepout === true)
|
|
222
|
+
.length
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Counts room-related design rules.
|
|
227
|
+
* @param {object[]} rules Parsed design rules.
|
|
228
|
+
* @returns {number}
|
|
229
|
+
*/
|
|
230
|
+
static #roomRuleCount(rules) {
|
|
231
|
+
return (rules || []).filter((rule) =>
|
|
232
|
+
PcbStatisticsBuilder.#isRoomRule(rule)
|
|
233
|
+
).length
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns true when a rule references a placement room.
|
|
238
|
+
* @param {object} rule Parsed design rule.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
static #isRoomRule(rule) {
|
|
242
|
+
const fields = [
|
|
243
|
+
rule?.ruleKind,
|
|
244
|
+
rule?.ruleType?.kind,
|
|
245
|
+
rule?.ruleType?.displayName,
|
|
246
|
+
rule?.scope1Expression,
|
|
247
|
+
rule?.scope2Expression
|
|
248
|
+
]
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.join(' ')
|
|
251
|
+
.toLowerCase()
|
|
252
|
+
|
|
253
|
+
return fields.includes('room')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Collects named placement rooms from rule scopes.
|
|
258
|
+
* @param {object[]} rules Parsed design rules.
|
|
259
|
+
* @returns {string[]}
|
|
260
|
+
*/
|
|
261
|
+
static #collectRoomNames(rules) {
|
|
262
|
+
const names = new Set()
|
|
263
|
+
|
|
264
|
+
for (const rule of rules || []) {
|
|
265
|
+
for (const scope of [rule?.scope1, rule?.scope2]) {
|
|
266
|
+
if (scope?.predicate !== 'WithinRoom') {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const roomName of scope.arguments || []) {
|
|
271
|
+
const normalized = String(roomName || '').trim()
|
|
272
|
+
if (normalized) {
|
|
273
|
+
names.add(normalized)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return [...names].sort((left, right) => left.localeCompare(right))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Builds a histogram of board-region layer-stack identities.
|
|
284
|
+
* @param {object[]} boardRegions Parsed board-region primitives.
|
|
285
|
+
* @returns {Record<string, number>}
|
|
286
|
+
*/
|
|
287
|
+
static #boardRegionLayerStacks(boardRegions) {
|
|
288
|
+
const histogram = {}
|
|
289
|
+
|
|
290
|
+
for (const region of boardRegions || []) {
|
|
291
|
+
const layerStack = String(
|
|
292
|
+
region?.substackName || region?.layerStackId || ''
|
|
293
|
+
).trim()
|
|
294
|
+
if (!layerStack) {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
histogram[layerStack] = (histogram[layerStack] || 0) + 1
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return histogram
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Collects layer ids mentioned by stack entries or primitives.
|
|
306
|
+
* @param {object} pcb Normalized PCB model.
|
|
307
|
+
* @returns {number[]}
|
|
308
|
+
*/
|
|
309
|
+
static #collectLayerIds(pcb) {
|
|
310
|
+
const layerIds = new Set()
|
|
311
|
+
|
|
312
|
+
for (const layer of pcb?.layers || []) {
|
|
313
|
+
if (Number.isInteger(layer.layerId)) {
|
|
314
|
+
layerIds.add(layer.layerId)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const family of PcbStatisticsBuilder.#primitiveFamilies()) {
|
|
319
|
+
for (const primitive of pcb?.[family] || []) {
|
|
320
|
+
if (Number.isInteger(primitive?.layerId)) {
|
|
321
|
+
layerIds.add(primitive.layerId)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...layerIds].sort((left, right) => left - right)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Counts primitives on one layer.
|
|
331
|
+
* @param {object} pcb Normalized PCB model.
|
|
332
|
+
* @param {number} layerId Numeric layer id.
|
|
333
|
+
* @returns {Record<string, number>}
|
|
334
|
+
*/
|
|
335
|
+
static #primitiveCountsForLayer(pcb, layerId) {
|
|
336
|
+
return Object.fromEntries(
|
|
337
|
+
PcbStatisticsBuilder.#primitiveFamilies().map((family) => [
|
|
338
|
+
family,
|
|
339
|
+
(pcb?.[family] || []).filter(
|
|
340
|
+
(primitive) => primitive?.layerId === layerId
|
|
341
|
+
).length
|
|
342
|
+
])
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Returns primitive families included in layer summaries.
|
|
348
|
+
* @returns {string[]}
|
|
349
|
+
*/
|
|
350
|
+
static #primitiveFamilies() {
|
|
351
|
+
return [
|
|
352
|
+
'tracks',
|
|
353
|
+
'arcs',
|
|
354
|
+
'vias',
|
|
355
|
+
'pads',
|
|
356
|
+
'fills',
|
|
357
|
+
'texts',
|
|
358
|
+
'regions',
|
|
359
|
+
'shapeBasedRegions'
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Finds a layer-stack entry by id.
|
|
365
|
+
* @param {object} pcb Normalized PCB model.
|
|
366
|
+
* @param {number} layerId Numeric layer id.
|
|
367
|
+
* @returns {object | null}
|
|
368
|
+
*/
|
|
369
|
+
static #findLayer(pcb, layerId) {
|
|
370
|
+
return (
|
|
371
|
+
(pcb?.layers || []).find((layer) => layer?.layerId === layerId) ||
|
|
372
|
+
null
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Finds a primitive-layer display name by id.
|
|
378
|
+
* @param {object} pcb Normalized PCB model.
|
|
379
|
+
* @param {number} layerId Numeric layer id.
|
|
380
|
+
* @returns {string}
|
|
381
|
+
*/
|
|
382
|
+
static #primitiveLayerName(pcb, layerId) {
|
|
383
|
+
return String(
|
|
384
|
+
(pcb?.primitiveLayers || []).find(
|
|
385
|
+
(layer) => layer?.layerId === layerId
|
|
386
|
+
)?.name || ''
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Resolves a compact layer role.
|
|
392
|
+
* @param {object | null} layer Layer-stack entry.
|
|
393
|
+
* @returns {string}
|
|
394
|
+
*/
|
|
395
|
+
static #layerRole(layer) {
|
|
396
|
+
const name = String(layer?.name || '').toLowerCase()
|
|
397
|
+
const kind = String(layer?.kind || layer?.role || '').toLowerCase()
|
|
398
|
+
|
|
399
|
+
if (kind.includes('dielectric')) return 'dielectric'
|
|
400
|
+
if (name.includes('mask')) return 'mask'
|
|
401
|
+
if (name.includes('paste')) return 'paste'
|
|
402
|
+
if (name.includes('silk') || name.includes('overlay')) return 'overlay'
|
|
403
|
+
if (name.includes('mechanical')) return 'mechanical'
|
|
404
|
+
|
|
405
|
+
return 'signal'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Builds aggregate layer-stack material and role statistics.
|
|
410
|
+
* @param {object[]} layers Layer-stack entries.
|
|
411
|
+
* @returns {object}
|
|
412
|
+
*/
|
|
413
|
+
static #layerMaterialSummary(layers) {
|
|
414
|
+
const summary = {
|
|
415
|
+
signalLayerCount: 0,
|
|
416
|
+
dielectricLayerCount: 0,
|
|
417
|
+
copperLayerCount: 0,
|
|
418
|
+
dielectricThicknessMil: 0,
|
|
419
|
+
materials: {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const layer of layers || []) {
|
|
423
|
+
const role = PcbStatisticsBuilder.#layerRole(layer)
|
|
424
|
+
const material = String(layer?.material || '').trim()
|
|
425
|
+
const kind = String(layer?.kind || '').toLowerCase()
|
|
426
|
+
|
|
427
|
+
if (role === 'signal') {
|
|
428
|
+
summary.signalLayerCount += 1
|
|
429
|
+
}
|
|
430
|
+
if (role === 'dielectric') {
|
|
431
|
+
summary.dielectricLayerCount += 1
|
|
432
|
+
summary.dielectricThicknessMil += Number(
|
|
433
|
+
layer?.thicknessMil || 0
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
if (
|
|
437
|
+
/copper/u.test(material.toLowerCase()) ||
|
|
438
|
+
/copper/u.test(kind)
|
|
439
|
+
) {
|
|
440
|
+
summary.copperLayerCount += 1
|
|
441
|
+
}
|
|
442
|
+
if (material) {
|
|
443
|
+
summary.materials[material] =
|
|
444
|
+
(summary.materials[material] || 0) + 1
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
summary.dielectricThicknessMil = PcbStatisticsBuilder.#round(
|
|
449
|
+
summary.dielectricThicknessMil
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return summary
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Returns true when a primitive has a drill.
|
|
457
|
+
* @param {object} primitive Primitive object.
|
|
458
|
+
* @returns {boolean}
|
|
459
|
+
*/
|
|
460
|
+
static #hasHole(primitive) {
|
|
461
|
+
return Number(primitive?.holeDiameter || 0) > 0
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Returns true when a drill is a slot.
|
|
466
|
+
* @param {object} primitive Primitive object.
|
|
467
|
+
* @returns {boolean}
|
|
468
|
+
*/
|
|
469
|
+
static #hasSlot(primitive) {
|
|
470
|
+
return (
|
|
471
|
+
Number(primitive?.holeSlotLength || primitive?.slotLength || 0) >
|
|
472
|
+
0 || Number(primitive?.holeShape || 0) === 2
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Builds a numeric histogram from values.
|
|
478
|
+
* @param {unknown[]} values Numeric values.
|
|
479
|
+
* @returns {Record<string, number>}
|
|
480
|
+
*/
|
|
481
|
+
static #histogram(values) {
|
|
482
|
+
const histogram = {}
|
|
483
|
+
|
|
484
|
+
for (const value of values || []) {
|
|
485
|
+
const number = PcbStatisticsBuilder.#round(value)
|
|
486
|
+
if (!Number.isFinite(number) || number === 0) {
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
histogram[String(number)] = (histogram[String(number)] || 0) + 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return histogram
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Rounds numeric values for stable JSON output.
|
|
498
|
+
* @param {unknown} value Numeric value.
|
|
499
|
+
* @returns {number}
|
|
500
|
+
*/
|
|
501
|
+
static #round(value) {
|
|
502
|
+
const number = Number(value || 0)
|
|
503
|
+
|
|
504
|
+
return Number.isFinite(number) ? Math.round(number * 1000) / 1000 : 0
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Rounds a numeric value only when it is present and finite.
|
|
509
|
+
* @param {unknown} value Candidate numeric value.
|
|
510
|
+
* @returns {number | undefined}
|
|
511
|
+
*/
|
|
512
|
+
static #optionalRound(value) {
|
|
513
|
+
const number = Number(value)
|
|
514
|
+
|
|
515
|
+
return Number.isFinite(number)
|
|
516
|
+
? Math.round(number * 1000) / 1000
|
|
517
|
+
: undefined
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Removes undefined values from one statistics object.
|
|
522
|
+
* @param {object} value Source object.
|
|
523
|
+
* @returns {object}
|
|
524
|
+
*/
|
|
525
|
+
static #stripUndefined(value) {
|
|
526
|
+
return Object.fromEntries(
|
|
527
|
+
Object.entries(value).filter(
|
|
528
|
+
([, entryValue]) => entryValue !== undefined
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
}
|