altium-toolkit 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic routed-net summaries from normalized PCB primitives.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbRouteAnalysisBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.pcb.route-analysis.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a route-analysis read model.
|
|
13
|
+
* @param {object} pcb Normalized PCB model and metadata.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(pcb = {}) {
|
|
17
|
+
const layerLookup = PcbRouteAnalysisBuilder.#layerLookup(pcb)
|
|
18
|
+
const routePrimitives = PcbRouteAnalysisBuilder.#routePrimitives(
|
|
19
|
+
pcb,
|
|
20
|
+
layerLookup
|
|
21
|
+
)
|
|
22
|
+
const viaRows = PcbRouteAnalysisBuilder.#viaRows(pcb, layerLookup)
|
|
23
|
+
const netRows = PcbRouteAnalysisBuilder.#netRows(
|
|
24
|
+
pcb,
|
|
25
|
+
routePrimitives,
|
|
26
|
+
viaRows
|
|
27
|
+
)
|
|
28
|
+
const summary = PcbRouteAnalysisBuilder.#summary(
|
|
29
|
+
pcb,
|
|
30
|
+
routePrimitives,
|
|
31
|
+
viaRows,
|
|
32
|
+
netRows
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
schema: PcbRouteAnalysisBuilder.SCHEMA,
|
|
37
|
+
units: {
|
|
38
|
+
coordinate: 'mil',
|
|
39
|
+
length: 'mil',
|
|
40
|
+
angle: 'deg'
|
|
41
|
+
},
|
|
42
|
+
summary,
|
|
43
|
+
byNet: netRows,
|
|
44
|
+
classes: PcbRouteAnalysisBuilder.#classRows(pcb, netRows),
|
|
45
|
+
differentialPairs: PcbRouteAnalysisBuilder.#differentialPairRows(
|
|
46
|
+
pcb,
|
|
47
|
+
netRows
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds layer descriptors keyed by id.
|
|
54
|
+
* @param {object} pcb Normalized PCB model.
|
|
55
|
+
* @returns {Map<number, object>}
|
|
56
|
+
*/
|
|
57
|
+
static #layerLookup(pcb) {
|
|
58
|
+
const lookup = new Map()
|
|
59
|
+
|
|
60
|
+
for (const layer of [
|
|
61
|
+
...(pcb?.layers || []),
|
|
62
|
+
...(pcb?.primitiveLayers || [])
|
|
63
|
+
]) {
|
|
64
|
+
const layerId = PcbRouteAnalysisBuilder.#layerId(layer)
|
|
65
|
+
if (!Number.isInteger(layerId) || lookup.has(layerId)) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
lookup.set(layerId, {
|
|
69
|
+
layerId,
|
|
70
|
+
layerKey: 'L' + layerId,
|
|
71
|
+
displayName:
|
|
72
|
+
layer.displayName || layer.name || 'Layer ' + layerId
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lookup
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds route primitive rows for tracks and arcs.
|
|
81
|
+
* @param {object} pcb Normalized PCB model.
|
|
82
|
+
* @param {Map<number, object>} layerLookup Layer lookup.
|
|
83
|
+
* @returns {object[]}
|
|
84
|
+
*/
|
|
85
|
+
static #routePrimitives(pcb, layerLookup) {
|
|
86
|
+
return [
|
|
87
|
+
...(pcb?.tracks || []).map((track, index) =>
|
|
88
|
+
PcbRouteAnalysisBuilder.#trackRow(track, index, layerLookup)
|
|
89
|
+
),
|
|
90
|
+
...(pcb?.arcs || []).map((arc, index) =>
|
|
91
|
+
PcbRouteAnalysisBuilder.#arcRow(arc, index, layerLookup)
|
|
92
|
+
)
|
|
93
|
+
].filter((primitive) => primitive.netName && primitive.lengthMil > 0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds one track route row.
|
|
98
|
+
* @param {object} track Track primitive.
|
|
99
|
+
* @param {number} index Track index.
|
|
100
|
+
* @param {Map<number, object>} layerLookup Layer lookup.
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
static #trackRow(track, index, layerLookup) {
|
|
104
|
+
const layer = PcbRouteAnalysisBuilder.#primitiveLayer(
|
|
105
|
+
track,
|
|
106
|
+
layerLookup
|
|
107
|
+
)
|
|
108
|
+
const start = PcbRouteAnalysisBuilder.#point(track.x1, track.y1)
|
|
109
|
+
const end = PcbRouteAnalysisBuilder.#point(track.x2, track.y2)
|
|
110
|
+
|
|
111
|
+
return PcbRouteAnalysisBuilder.#stripEmpty({
|
|
112
|
+
primitiveKey: 'track-' + index,
|
|
113
|
+
kind: 'track',
|
|
114
|
+
netName: PcbRouteAnalysisBuilder.#netName(track),
|
|
115
|
+
layerId: layer?.layerId,
|
|
116
|
+
layerKey: layer?.layerKey,
|
|
117
|
+
layerDisplayName: layer?.displayName,
|
|
118
|
+
lengthMil: PcbRouteAnalysisBuilder.#round(
|
|
119
|
+
PcbRouteAnalysisBuilder.#distance(start, end)
|
|
120
|
+
),
|
|
121
|
+
endpoints: [start, end]
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds one arc route row.
|
|
127
|
+
* @param {object} arc Arc primitive.
|
|
128
|
+
* @param {number} index Arc index.
|
|
129
|
+
* @param {Map<number, object>} layerLookup Layer lookup.
|
|
130
|
+
* @returns {object}
|
|
131
|
+
*/
|
|
132
|
+
static #arcRow(arc, index, layerLookup) {
|
|
133
|
+
const layer = PcbRouteAnalysisBuilder.#primitiveLayer(arc, layerLookup)
|
|
134
|
+
const endpoints = PcbRouteAnalysisBuilder.#arcEndpoints(arc)
|
|
135
|
+
|
|
136
|
+
return PcbRouteAnalysisBuilder.#stripEmpty({
|
|
137
|
+
primitiveKey: 'arc-' + index,
|
|
138
|
+
kind: 'arc',
|
|
139
|
+
netName: PcbRouteAnalysisBuilder.#netName(arc),
|
|
140
|
+
layerId: layer?.layerId,
|
|
141
|
+
layerKey: layer?.layerKey,
|
|
142
|
+
layerDisplayName: layer?.displayName,
|
|
143
|
+
lengthMil: PcbRouteAnalysisBuilder.#round(
|
|
144
|
+
Number(arc.radius || 0) *
|
|
145
|
+
Math.abs(PcbRouteAnalysisBuilder.#arcSweepRadians(arc))
|
|
146
|
+
),
|
|
147
|
+
endpoints
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds via participation rows.
|
|
153
|
+
* @param {object} pcb Normalized PCB model.
|
|
154
|
+
* @param {Map<number, object>} layerLookup Layer lookup.
|
|
155
|
+
* @returns {object[]}
|
|
156
|
+
*/
|
|
157
|
+
static #viaRows(pcb, layerLookup) {
|
|
158
|
+
return (pcb?.vias || [])
|
|
159
|
+
.map((via, index) => {
|
|
160
|
+
const layer = PcbRouteAnalysisBuilder.#primitiveLayer(
|
|
161
|
+
via,
|
|
162
|
+
layerLookup
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return PcbRouteAnalysisBuilder.#stripEmpty({
|
|
166
|
+
primitiveKey: 'via-' + index,
|
|
167
|
+
kind: 'via',
|
|
168
|
+
netName: PcbRouteAnalysisBuilder.#netName(via),
|
|
169
|
+
layerId: layer?.layerId,
|
|
170
|
+
layerKey: layer?.layerKey,
|
|
171
|
+
layerDisplayName: layer?.displayName,
|
|
172
|
+
point: PcbRouteAnalysisBuilder.#point(via.x, via.y)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
.filter((via) => via.netName)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Builds deterministic net route rows.
|
|
180
|
+
* @param {object} pcb Normalized PCB model.
|
|
181
|
+
* @param {object[]} routePrimitives Route primitives.
|
|
182
|
+
* @param {object[]} viaRows Via rows.
|
|
183
|
+
* @returns {object[]}
|
|
184
|
+
*/
|
|
185
|
+
static #netRows(pcb, routePrimitives, viaRows) {
|
|
186
|
+
const netNames = new Set([
|
|
187
|
+
...(pcb?.nets || []).map((net) => net.name).filter(Boolean),
|
|
188
|
+
...routePrimitives.map((primitive) => primitive.netName),
|
|
189
|
+
...viaRows.map((via) => via.netName)
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
return [...netNames]
|
|
193
|
+
.map((netName) =>
|
|
194
|
+
PcbRouteAnalysisBuilder.#netRow(
|
|
195
|
+
netName,
|
|
196
|
+
routePrimitives.filter(
|
|
197
|
+
(primitive) => primitive.netName === netName
|
|
198
|
+
),
|
|
199
|
+
viaRows.filter((via) => via.netName === netName)
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
.filter((net) => net.totalLengthMil > 0 || net.viaCount > 0)
|
|
203
|
+
.sort((left, right) =>
|
|
204
|
+
left.netName.localeCompare(right.netName, undefined, {
|
|
205
|
+
numeric: true
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Builds one net route row.
|
|
212
|
+
* @param {string} netName Net name.
|
|
213
|
+
* @param {object[]} primitives Route primitives.
|
|
214
|
+
* @param {object[]} vias Via rows.
|
|
215
|
+
* @returns {object}
|
|
216
|
+
*/
|
|
217
|
+
static #netRow(netName, primitives, vias) {
|
|
218
|
+
const trackRows = primitives.filter(
|
|
219
|
+
(primitive) => primitive.kind === 'track'
|
|
220
|
+
)
|
|
221
|
+
const arcRows = primitives.filter(
|
|
222
|
+
(primitive) => primitive.kind === 'arc'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
netName,
|
|
227
|
+
totalLengthMil: PcbRouteAnalysisBuilder.#sumLength(primitives),
|
|
228
|
+
trackLengthMil: PcbRouteAnalysisBuilder.#sumLength(trackRows),
|
|
229
|
+
arcLengthMil: PcbRouteAnalysisBuilder.#sumLength(arcRows),
|
|
230
|
+
trackCount: trackRows.length,
|
|
231
|
+
arcCount: arcRows.length,
|
|
232
|
+
viaCount: vias.length,
|
|
233
|
+
layers: PcbRouteAnalysisBuilder.#layerKeys(primitives, vias),
|
|
234
|
+
layerParticipation: PcbRouteAnalysisBuilder.#layerParticipation(
|
|
235
|
+
primitives,
|
|
236
|
+
vias
|
|
237
|
+
),
|
|
238
|
+
connectedRouteGroups:
|
|
239
|
+
PcbRouteAnalysisBuilder.#connectedRouteGroups(primitives)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Builds per-net-class length summaries.
|
|
245
|
+
* @param {object} pcb Normalized PCB model.
|
|
246
|
+
* @param {object[]} netRows Net rows.
|
|
247
|
+
* @returns {object[]}
|
|
248
|
+
*/
|
|
249
|
+
static #classRows(pcb, netRows) {
|
|
250
|
+
const lengthByNet = PcbRouteAnalysisBuilder.#lengthByNet(netRows)
|
|
251
|
+
const knownNetNames = new Set(netRows.map((net) => net.netName))
|
|
252
|
+
|
|
253
|
+
return (pcb?.classes || [])
|
|
254
|
+
.filter((classRecord) =>
|
|
255
|
+
PcbRouteAnalysisBuilder.#isNetClass(classRecord)
|
|
256
|
+
)
|
|
257
|
+
.map((classRecord) => {
|
|
258
|
+
const netNames = (classRecord.members || [])
|
|
259
|
+
.filter((member) => knownNetNames.has(member))
|
|
260
|
+
.sort((left, right) =>
|
|
261
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return PcbRouteAnalysisBuilder.#stripEmpty({
|
|
265
|
+
name: classRecord.name,
|
|
266
|
+
kindName: classRecord.kindName,
|
|
267
|
+
members: [...(classRecord.members || [])],
|
|
268
|
+
netNames,
|
|
269
|
+
totalLengthMil: PcbRouteAnalysisBuilder.#round(
|
|
270
|
+
netNames.reduce(
|
|
271
|
+
(total, netName) =>
|
|
272
|
+
total + Number(lengthByNet.get(netName) || 0),
|
|
273
|
+
0
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
.filter((classRow) => classRow.name && classRow.netNames?.length)
|
|
279
|
+
.sort((left, right) =>
|
|
280
|
+
left.name.localeCompare(right.name, undefined, {
|
|
281
|
+
numeric: true
|
|
282
|
+
})
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Builds differential-pair route summaries.
|
|
288
|
+
* @param {object} pcb Normalized PCB model.
|
|
289
|
+
* @param {object[]} netRows Net rows.
|
|
290
|
+
* @returns {object[]}
|
|
291
|
+
*/
|
|
292
|
+
static #differentialPairRows(pcb, netRows) {
|
|
293
|
+
const lengthByNet = PcbRouteAnalysisBuilder.#lengthByNet(netRows)
|
|
294
|
+
|
|
295
|
+
return (pcb?.differentialPairs || [])
|
|
296
|
+
.map((pair) => {
|
|
297
|
+
const positiveLength = Number(
|
|
298
|
+
lengthByNet.get(pair.positiveNetName) || 0
|
|
299
|
+
)
|
|
300
|
+
const negativeLength = Number(
|
|
301
|
+
lengthByNet.get(pair.negativeNetName) || 0
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return PcbRouteAnalysisBuilder.#stripEmpty({
|
|
305
|
+
name: pair.name,
|
|
306
|
+
positiveNetName: pair.positiveNetName,
|
|
307
|
+
negativeNetName: pair.negativeNetName,
|
|
308
|
+
positiveLengthMil:
|
|
309
|
+
PcbRouteAnalysisBuilder.#round(positiveLength),
|
|
310
|
+
negativeLengthMil:
|
|
311
|
+
PcbRouteAnalysisBuilder.#round(negativeLength),
|
|
312
|
+
skewLengthMil: PcbRouteAnalysisBuilder.#round(
|
|
313
|
+
Math.abs(positiveLength - negativeLength)
|
|
314
|
+
),
|
|
315
|
+
classes: pair.classNames || []
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
.filter(
|
|
319
|
+
(pair) =>
|
|
320
|
+
pair.name &&
|
|
321
|
+
(pair.positiveLengthMil > 0 || pair.negativeLengthMil > 0)
|
|
322
|
+
)
|
|
323
|
+
.sort((left, right) =>
|
|
324
|
+
left.name.localeCompare(right.name, undefined, {
|
|
325
|
+
numeric: true
|
|
326
|
+
})
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Builds top-level route summary counters.
|
|
332
|
+
* @param {object} pcb Normalized PCB model.
|
|
333
|
+
* @param {object[]} routePrimitives Route primitives.
|
|
334
|
+
* @param {object[]} viaRows Via rows.
|
|
335
|
+
* @param {object[]} netRows Net rows.
|
|
336
|
+
* @returns {object}
|
|
337
|
+
*/
|
|
338
|
+
static #summary(pcb, routePrimitives, viaRows, netRows) {
|
|
339
|
+
return {
|
|
340
|
+
netCount: (pcb?.nets || []).length || netRows.length,
|
|
341
|
+
routedNetCount: netRows.length,
|
|
342
|
+
totalLengthMil: PcbRouteAnalysisBuilder.#sumLength(routePrimitives),
|
|
343
|
+
trackCount: routePrimitives.filter(
|
|
344
|
+
(primitive) => primitive.kind === 'track'
|
|
345
|
+
).length,
|
|
346
|
+
arcCount: routePrimitives.filter(
|
|
347
|
+
(primitive) => primitive.kind === 'arc'
|
|
348
|
+
).length,
|
|
349
|
+
viaCount: viaRows.length,
|
|
350
|
+
connectedRouteGroupCount: netRows.reduce(
|
|
351
|
+
(total, net) => total + net.connectedRouteGroups.length,
|
|
352
|
+
0
|
|
353
|
+
),
|
|
354
|
+
differentialPairCount: (pcb?.differentialPairs || []).filter(
|
|
355
|
+
(pair) =>
|
|
356
|
+
netRows.some(
|
|
357
|
+
(net) =>
|
|
358
|
+
net.netName === pair.positiveNetName ||
|
|
359
|
+
net.netName === pair.negativeNetName
|
|
360
|
+
)
|
|
361
|
+
).length
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds connected route groups from shared endpoints.
|
|
367
|
+
* @param {object[]} primitives Route primitives for one net.
|
|
368
|
+
* @returns {object[]}
|
|
369
|
+
*/
|
|
370
|
+
static #connectedRouteGroups(primitives) {
|
|
371
|
+
const parent = primitives.map((_, index) => index)
|
|
372
|
+
const endpointOwners = new Map()
|
|
373
|
+
|
|
374
|
+
primitives.forEach((primitive, primitiveIndex) => {
|
|
375
|
+
for (const endpoint of primitive.endpoints || []) {
|
|
376
|
+
const key = PcbRouteAnalysisBuilder.#pointKey(endpoint)
|
|
377
|
+
if (endpointOwners.has(key)) {
|
|
378
|
+
PcbRouteAnalysisBuilder.#union(
|
|
379
|
+
parent,
|
|
380
|
+
primitiveIndex,
|
|
381
|
+
endpointOwners.get(key)
|
|
382
|
+
)
|
|
383
|
+
} else {
|
|
384
|
+
endpointOwners.set(key, primitiveIndex)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const groups = new Map()
|
|
390
|
+
primitives.forEach((primitive, primitiveIndex) => {
|
|
391
|
+
const groupIndex = PcbRouteAnalysisBuilder.#find(
|
|
392
|
+
parent,
|
|
393
|
+
primitiveIndex
|
|
394
|
+
)
|
|
395
|
+
if (!groups.has(groupIndex)) {
|
|
396
|
+
groups.set(groupIndex, [])
|
|
397
|
+
}
|
|
398
|
+
groups.get(groupIndex).push(primitive)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
return [...groups.values()]
|
|
402
|
+
.map((group, index) => ({
|
|
403
|
+
groupIndex: index,
|
|
404
|
+
primitiveKeys: group.map((primitive) => primitive.primitiveKey),
|
|
405
|
+
lengthMil: PcbRouteAnalysisBuilder.#sumLength(group),
|
|
406
|
+
layerIds: PcbRouteAnalysisBuilder.#layerIds(group, []),
|
|
407
|
+
layerKeys: PcbRouteAnalysisBuilder.#layerKeys(group, []),
|
|
408
|
+
endpoints: PcbRouteAnalysisBuilder.#groupEndpoints(group)
|
|
409
|
+
}))
|
|
410
|
+
.sort((left, right) => left.groupIndex - right.groupIndex)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Builds layer participation rows.
|
|
415
|
+
* @param {object[]} primitives Route primitives.
|
|
416
|
+
* @param {object[]} vias Via rows.
|
|
417
|
+
* @returns {object[]}
|
|
418
|
+
*/
|
|
419
|
+
static #layerParticipation(primitives, vias) {
|
|
420
|
+
const rowsByKey = new Map()
|
|
421
|
+
|
|
422
|
+
for (const row of [...primitives, ...vias]) {
|
|
423
|
+
const layerKey = row.layerKey || ''
|
|
424
|
+
if (!layerKey) {
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
if (!rowsByKey.has(layerKey)) {
|
|
428
|
+
rowsByKey.set(layerKey, {
|
|
429
|
+
layerId: row.layerId,
|
|
430
|
+
layerKey,
|
|
431
|
+
displayName: row.layerDisplayName,
|
|
432
|
+
totalLengthMil: 0,
|
|
433
|
+
primitiveCount: 0,
|
|
434
|
+
viaCount: 0
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
const entry = rowsByKey.get(layerKey)
|
|
438
|
+
entry.totalLengthMil += Number(row.lengthMil || 0)
|
|
439
|
+
entry.primitiveCount += row.kind === 'via' ? 0 : 1
|
|
440
|
+
entry.viaCount += row.kind === 'via' ? 1 : 0
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return [...rowsByKey.values()]
|
|
444
|
+
.map((entry) => ({
|
|
445
|
+
...entry,
|
|
446
|
+
totalLengthMil: PcbRouteAnalysisBuilder.#round(
|
|
447
|
+
entry.totalLengthMil
|
|
448
|
+
)
|
|
449
|
+
}))
|
|
450
|
+
.sort((left, right) =>
|
|
451
|
+
left.layerKey.localeCompare(right.layerKey, undefined, {
|
|
452
|
+
numeric: true
|
|
453
|
+
})
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Returns layer keys used by primitive rows.
|
|
459
|
+
* @param {object[]} primitives Route primitives.
|
|
460
|
+
* @param {object[]} vias Via rows.
|
|
461
|
+
* @returns {string[]}
|
|
462
|
+
*/
|
|
463
|
+
static #layerKeys(primitives, vias) {
|
|
464
|
+
return [
|
|
465
|
+
...new Set(
|
|
466
|
+
[...primitives, ...vias]
|
|
467
|
+
.map((row) => row.layerKey)
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
)
|
|
470
|
+
].sort((left, right) =>
|
|
471
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Returns layer ids used by primitive rows.
|
|
477
|
+
* @param {object[]} primitives Route primitives.
|
|
478
|
+
* @param {object[]} vias Via rows.
|
|
479
|
+
* @returns {number[]}
|
|
480
|
+
*/
|
|
481
|
+
static #layerIds(primitives, vias) {
|
|
482
|
+
return [
|
|
483
|
+
...new Set(
|
|
484
|
+
[...primitives, ...vias]
|
|
485
|
+
.map((row) => row.layerId)
|
|
486
|
+
.filter((layerId) => Number.isInteger(layerId))
|
|
487
|
+
)
|
|
488
|
+
].sort((left, right) => left - right)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Builds deduplicated group endpoint rows.
|
|
493
|
+
* @param {object[]} group Route group primitives.
|
|
494
|
+
* @returns {object[]}
|
|
495
|
+
*/
|
|
496
|
+
static #groupEndpoints(group) {
|
|
497
|
+
const endpointsByKey = new Map()
|
|
498
|
+
for (const primitive of group) {
|
|
499
|
+
for (const endpoint of primitive.endpoints || []) {
|
|
500
|
+
endpointsByKey.set(
|
|
501
|
+
PcbRouteAnalysisBuilder.#pointKey(endpoint),
|
|
502
|
+
endpoint
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return [...endpointsByKey.values()]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Resolves one primitive layer descriptor.
|
|
511
|
+
* @param {object} primitive Primitive row.
|
|
512
|
+
* @param {Map<number, object>} layerLookup Layer lookup.
|
|
513
|
+
* @returns {object | null}
|
|
514
|
+
*/
|
|
515
|
+
static #primitiveLayer(primitive, layerLookup) {
|
|
516
|
+
const layerId = PcbRouteAnalysisBuilder.#layerId(primitive)
|
|
517
|
+
if (!Number.isInteger(layerId)) {
|
|
518
|
+
return null
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return (
|
|
522
|
+
layerLookup.get(layerId) || {
|
|
523
|
+
layerId,
|
|
524
|
+
layerKey: 'L' + layerId,
|
|
525
|
+
displayName: 'Layer ' + layerId
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Resolves a layer id from several native field spellings.
|
|
532
|
+
* @param {object} value Candidate object.
|
|
533
|
+
* @returns {number | undefined}
|
|
534
|
+
*/
|
|
535
|
+
static #layerId(value) {
|
|
536
|
+
for (const key of ['layerId', 'layerCode', 'id', 'index']) {
|
|
537
|
+
const number = Number(value?.[key])
|
|
538
|
+
if (Number.isInteger(number)) {
|
|
539
|
+
return number
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return undefined
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Resolves a primitive net name.
|
|
548
|
+
* @param {object} primitive Primitive row.
|
|
549
|
+
* @returns {string}
|
|
550
|
+
*/
|
|
551
|
+
static #netName(primitive) {
|
|
552
|
+
return String(
|
|
553
|
+
primitive?.netName || primitive?.net || primitive?.netLabel || ''
|
|
554
|
+
).trim()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Builds a normalized point.
|
|
559
|
+
* @param {unknown} x X coordinate.
|
|
560
|
+
* @param {unknown} y Y coordinate.
|
|
561
|
+
* @returns {{ x: number, y: number }}
|
|
562
|
+
*/
|
|
563
|
+
static #point(x, y) {
|
|
564
|
+
return {
|
|
565
|
+
x: PcbRouteAnalysisBuilder.#round(x),
|
|
566
|
+
y: PcbRouteAnalysisBuilder.#round(y)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Computes Euclidean distance between two points.
|
|
572
|
+
* @param {{ x: number, y: number }} start Start point.
|
|
573
|
+
* @param {{ x: number, y: number }} end End point.
|
|
574
|
+
* @returns {number}
|
|
575
|
+
*/
|
|
576
|
+
static #distance(start, end) {
|
|
577
|
+
return Math.hypot(
|
|
578
|
+
Number(end.x) - Number(start.x),
|
|
579
|
+
Number(end.y) - Number(start.y)
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Builds approximate endpoints for an arc primitive.
|
|
585
|
+
* @param {object} arc Arc primitive.
|
|
586
|
+
* @returns {object[]}
|
|
587
|
+
*/
|
|
588
|
+
static #arcEndpoints(arc) {
|
|
589
|
+
const radius = Number(arc.radius || 0)
|
|
590
|
+
const centerX = Number(arc.x || arc.centerX || 0)
|
|
591
|
+
const centerY = Number(arc.y || arc.centerY || 0)
|
|
592
|
+
const startAngle = (Number(arc.startAngle || 0) * Math.PI) / 180
|
|
593
|
+
const endAngle = (Number(arc.endAngle || 0) * Math.PI) / 180
|
|
594
|
+
|
|
595
|
+
return [
|
|
596
|
+
PcbRouteAnalysisBuilder.#point(
|
|
597
|
+
centerX + radius * Math.cos(startAngle),
|
|
598
|
+
centerY + radius * Math.sin(startAngle)
|
|
599
|
+
),
|
|
600
|
+
PcbRouteAnalysisBuilder.#point(
|
|
601
|
+
centerX + radius * Math.cos(endAngle),
|
|
602
|
+
centerY + radius * Math.sin(endAngle)
|
|
603
|
+
)
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Resolves an arc sweep in radians.
|
|
609
|
+
* @param {object} arc Arc primitive.
|
|
610
|
+
* @returns {number}
|
|
611
|
+
*/
|
|
612
|
+
static #arcSweepRadians(arc) {
|
|
613
|
+
if (Number.isFinite(Number(arc.sweepAngle))) {
|
|
614
|
+
return (Number(arc.sweepAngle) * Math.PI) / 180
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const start = Number(arc.startAngle || 0)
|
|
618
|
+
const end = Number(arc.endAngle || 0)
|
|
619
|
+
let sweep = end - start
|
|
620
|
+
while (sweep <= -180) sweep += 360
|
|
621
|
+
while (sweep > 180) sweep -= 360
|
|
622
|
+
return (sweep * Math.PI) / 180
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Builds a stable endpoint key with small coordinate tolerance.
|
|
627
|
+
* @param {{ x: number, y: number }} point Endpoint.
|
|
628
|
+
* @returns {string}
|
|
629
|
+
*/
|
|
630
|
+
static #pointKey(point) {
|
|
631
|
+
return (
|
|
632
|
+
PcbRouteAnalysisBuilder.#round(point.x).toFixed(3) +
|
|
633
|
+
',' +
|
|
634
|
+
PcbRouteAnalysisBuilder.#round(point.y).toFixed(3)
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Returns a map of net names to routed length.
|
|
640
|
+
* @param {object[]} netRows Net rows.
|
|
641
|
+
* @returns {Map<string, number>}
|
|
642
|
+
*/
|
|
643
|
+
static #lengthByNet(netRows) {
|
|
644
|
+
return new Map(
|
|
645
|
+
netRows.map((net) => [net.netName, Number(net.totalLengthMil || 0)])
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Sums route primitive lengths.
|
|
651
|
+
* @param {object[]} primitives Route primitive rows.
|
|
652
|
+
* @returns {number}
|
|
653
|
+
*/
|
|
654
|
+
static #sumLength(primitives) {
|
|
655
|
+
return PcbRouteAnalysisBuilder.#round(
|
|
656
|
+
(primitives || []).reduce(
|
|
657
|
+
(total, primitive) => total + Number(primitive.lengthMil || 0),
|
|
658
|
+
0
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Returns true when a class describes nets.
|
|
665
|
+
* @param {object} classRecord Class row.
|
|
666
|
+
* @returns {boolean}
|
|
667
|
+
*/
|
|
668
|
+
static #isNetClass(classRecord) {
|
|
669
|
+
return (
|
|
670
|
+
classRecord?.kindName === 'net' || Number(classRecord?.kind) === 0
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Finds a union-find root.
|
|
676
|
+
* @param {number[]} parent Parent table.
|
|
677
|
+
* @param {number} index Entry index.
|
|
678
|
+
* @returns {number}
|
|
679
|
+
*/
|
|
680
|
+
static #find(parent, index) {
|
|
681
|
+
if (parent[index] !== index) {
|
|
682
|
+
parent[index] = PcbRouteAnalysisBuilder.#find(parent, parent[index])
|
|
683
|
+
}
|
|
684
|
+
return parent[index]
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Unions two route group indexes.
|
|
689
|
+
* @param {number[]} parent Parent table.
|
|
690
|
+
* @param {number} left Left index.
|
|
691
|
+
* @param {number} right Right index.
|
|
692
|
+
*/
|
|
693
|
+
static #union(parent, left, right) {
|
|
694
|
+
const leftRoot = PcbRouteAnalysisBuilder.#find(parent, left)
|
|
695
|
+
const rightRoot = PcbRouteAnalysisBuilder.#find(parent, right)
|
|
696
|
+
if (leftRoot !== rightRoot) {
|
|
697
|
+
parent[rightRoot] = leftRoot
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Rounds numeric values for stable JSON.
|
|
703
|
+
* @param {unknown} value Candidate numeric value.
|
|
704
|
+
* @returns {number}
|
|
705
|
+
*/
|
|
706
|
+
static #round(value) {
|
|
707
|
+
const number = Number(value || 0)
|
|
708
|
+
return Number.isFinite(number) ? Math.round(number * 1000) / 1000 : 0
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Removes empty object fields while preserving false and zero.
|
|
713
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
714
|
+
* @returns {Record<string, unknown>}
|
|
715
|
+
*/
|
|
716
|
+
static #stripEmpty(value) {
|
|
717
|
+
return Object.fromEntries(
|
|
718
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
719
|
+
if (Array.isArray(entryValue)) {
|
|
720
|
+
return entryValue.length > 0
|
|
721
|
+
}
|
|
722
|
+
return (
|
|
723
|
+
entryValue !== null &&
|
|
724
|
+
entryValue !== undefined &&
|
|
725
|
+
entryValue !== ''
|
|
726
|
+
)
|
|
727
|
+
})
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
}
|