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,298 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds route-highlight profile rows for PCB review metadata.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbReviewRouteHighlightProfileBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Builds route-highlight profiles for classes, pairs, pair classes, and nets.
|
|
11
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static build(routeAnalysis = {}) {
|
|
15
|
+
return [
|
|
16
|
+
...PcbReviewRouteHighlightProfileBuilder.#differentialPairProfiles(
|
|
17
|
+
routeAnalysis
|
|
18
|
+
),
|
|
19
|
+
...PcbReviewRouteHighlightProfileBuilder.#differentialPairClassProfiles(
|
|
20
|
+
routeAnalysis
|
|
21
|
+
),
|
|
22
|
+
...PcbReviewRouteHighlightProfileBuilder.#netClassProfiles(
|
|
23
|
+
routeAnalysis
|
|
24
|
+
),
|
|
25
|
+
...PcbReviewRouteHighlightProfileBuilder.#netProfiles(routeAnalysis)
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds net-class highlight profiles.
|
|
31
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
32
|
+
* @returns {object[]}
|
|
33
|
+
*/
|
|
34
|
+
static #netClassProfiles(routeAnalysis) {
|
|
35
|
+
return (routeAnalysis.classes || []).map((classRow) =>
|
|
36
|
+
PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
|
|
37
|
+
selectorKind: 'net-class',
|
|
38
|
+
keyPrefix: 'highlight-net-class-',
|
|
39
|
+
name: classRow.name,
|
|
40
|
+
netNames: classRow.netNames || [],
|
|
41
|
+
routeAnalysis
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds differential-pair highlight profiles.
|
|
48
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
49
|
+
* @returns {object[]}
|
|
50
|
+
*/
|
|
51
|
+
static #differentialPairProfiles(routeAnalysis) {
|
|
52
|
+
return (routeAnalysis.differentialPairs || []).map((pair) =>
|
|
53
|
+
PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
|
|
54
|
+
selectorKind: 'differential-pair',
|
|
55
|
+
keyPrefix: 'highlight-diff-pair-',
|
|
56
|
+
name: pair.name,
|
|
57
|
+
netNames: [pair.positiveNetName, pair.negativeNetName].filter(
|
|
58
|
+
Boolean
|
|
59
|
+
),
|
|
60
|
+
routeAnalysis
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds differential-pair class highlight profiles.
|
|
67
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
68
|
+
* @returns {object[]}
|
|
69
|
+
*/
|
|
70
|
+
static #differentialPairClassProfiles(routeAnalysis) {
|
|
71
|
+
const classNames = new Map()
|
|
72
|
+
for (const pair of routeAnalysis.differentialPairs || []) {
|
|
73
|
+
for (const className of pair.classes || []) {
|
|
74
|
+
if (!classNames.has(className)) {
|
|
75
|
+
classNames.set(className, new Set())
|
|
76
|
+
}
|
|
77
|
+
const netNames = classNames.get(className)
|
|
78
|
+
for (const netName of [
|
|
79
|
+
pair.positiveNetName,
|
|
80
|
+
pair.negativeNetName
|
|
81
|
+
]) {
|
|
82
|
+
if (netName) {
|
|
83
|
+
netNames.add(netName)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...classNames.entries()]
|
|
90
|
+
.sort(([left], [right]) =>
|
|
91
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
92
|
+
)
|
|
93
|
+
.map(([className, netNames]) =>
|
|
94
|
+
PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
|
|
95
|
+
selectorKind: 'differential-pair-class',
|
|
96
|
+
keyPrefix: 'highlight-diff-pair-class-',
|
|
97
|
+
name: className,
|
|
98
|
+
netNames: [...netNames],
|
|
99
|
+
routeAnalysis
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Builds scalar net highlight profiles.
|
|
106
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
107
|
+
* @returns {object[]}
|
|
108
|
+
*/
|
|
109
|
+
static #netProfiles(routeAnalysis) {
|
|
110
|
+
return (routeAnalysis.byNet || []).map((net) =>
|
|
111
|
+
PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
|
|
112
|
+
selectorKind: 'net',
|
|
113
|
+
keyPrefix: 'highlight-net-',
|
|
114
|
+
name: net.netName,
|
|
115
|
+
netNames: [net.netName],
|
|
116
|
+
routeAnalysis
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds one route-highlight profile.
|
|
123
|
+
* @param {{ selectorKind: string, keyPrefix: string, name: string, netNames: string[], routeAnalysis: object }} options Profile options.
|
|
124
|
+
* @returns {object}
|
|
125
|
+
*/
|
|
126
|
+
static #highlightProfile(options) {
|
|
127
|
+
const netNames = PcbReviewRouteHighlightProfileBuilder.#sortedStrings(
|
|
128
|
+
options.netNames || []
|
|
129
|
+
)
|
|
130
|
+
const layerGroups = PcbReviewRouteHighlightProfileBuilder.#layerGroups(
|
|
131
|
+
options.routeAnalysis,
|
|
132
|
+
netNames
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return PcbReviewRouteHighlightProfileBuilder.#stripEmpty({
|
|
136
|
+
key:
|
|
137
|
+
options.keyPrefix +
|
|
138
|
+
PcbReviewRouteHighlightProfileBuilder.#slug(options.name),
|
|
139
|
+
selectorKind: options.selectorKind,
|
|
140
|
+
name: options.name,
|
|
141
|
+
netNames,
|
|
142
|
+
minRoutedLengthMil:
|
|
143
|
+
layerGroups.length > 0
|
|
144
|
+
? Math.min(
|
|
145
|
+
...layerGroups.map((group) =>
|
|
146
|
+
Number(group.routedLengthMil || 0)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
: 0,
|
|
150
|
+
layerGroups,
|
|
151
|
+
style: PcbReviewRouteHighlightProfileBuilder.#highlightStyle(
|
|
152
|
+
options.selectorKind
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Builds per-layer route-highlight groups.
|
|
159
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
160
|
+
* @param {string[]} netNames Net names.
|
|
161
|
+
* @returns {object[]}
|
|
162
|
+
*/
|
|
163
|
+
static #layerGroups(routeAnalysis, netNames) {
|
|
164
|
+
const groupsByLayer = new Map()
|
|
165
|
+
for (const net of PcbReviewRouteHighlightProfileBuilder.#netsByName(
|
|
166
|
+
routeAnalysis,
|
|
167
|
+
netNames
|
|
168
|
+
)) {
|
|
169
|
+
for (const participation of net.layerParticipation || []) {
|
|
170
|
+
const layerKey = participation.layerKey || ''
|
|
171
|
+
if (!layerKey) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (!groupsByLayer.has(layerKey)) {
|
|
175
|
+
groupsByLayer.set(layerKey, {
|
|
176
|
+
layerKey,
|
|
177
|
+
primitiveKeys: new Set(),
|
|
178
|
+
routedLengthMil: 0
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
const group = groupsByLayer.get(layerKey)
|
|
182
|
+
group.routedLengthMil += Number(
|
|
183
|
+
participation.totalLengthMil || 0
|
|
184
|
+
)
|
|
185
|
+
for (const routeGroup of net.connectedRouteGroups || []) {
|
|
186
|
+
if (!(routeGroup.layerKeys || []).includes(layerKey)) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
for (const primitiveKey of routeGroup.primitiveKeys || []) {
|
|
190
|
+
group.primitiveKeys.add(primitiveKey)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return [...groupsByLayer.values()]
|
|
197
|
+
.map((group) => ({
|
|
198
|
+
layerKey: group.layerKey,
|
|
199
|
+
primitiveKeys:
|
|
200
|
+
PcbReviewRouteHighlightProfileBuilder.#sortedStrings([
|
|
201
|
+
...group.primitiveKeys
|
|
202
|
+
]),
|
|
203
|
+
routedLengthMil: PcbReviewRouteHighlightProfileBuilder.#round(
|
|
204
|
+
group.routedLengthMil
|
|
205
|
+
)
|
|
206
|
+
}))
|
|
207
|
+
.sort((left, right) =>
|
|
208
|
+
left.layerKey.localeCompare(right.layerKey, undefined, {
|
|
209
|
+
numeric: true
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolves net route rows by name.
|
|
216
|
+
* @param {object} routeAnalysis Route analysis model.
|
|
217
|
+
* @param {string[]} netNames Net names.
|
|
218
|
+
* @returns {object[]}
|
|
219
|
+
*/
|
|
220
|
+
static #netsByName(routeAnalysis, netNames) {
|
|
221
|
+
const wanted = new Set(netNames || [])
|
|
222
|
+
return (routeAnalysis.byNet || []).filter((net) =>
|
|
223
|
+
wanted.has(net.netName)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Returns deterministic highlight style metadata.
|
|
229
|
+
* @param {string} selectorKind Selector kind.
|
|
230
|
+
* @returns {{ highlightColor: string, contextColor: string }}
|
|
231
|
+
*/
|
|
232
|
+
static #highlightStyle(selectorKind) {
|
|
233
|
+
if (selectorKind === 'differential-pair') {
|
|
234
|
+
return { highlightColor: '#dc2626', contextColor: '#475569' }
|
|
235
|
+
}
|
|
236
|
+
if (selectorKind === 'differential-pair-class') {
|
|
237
|
+
return { highlightColor: '#7c3aed', contextColor: '#475569' }
|
|
238
|
+
}
|
|
239
|
+
if (selectorKind === 'net-class') {
|
|
240
|
+
return { highlightColor: '#d97706', contextColor: '#475569' }
|
|
241
|
+
}
|
|
242
|
+
return { highlightColor: '#2563eb', contextColor: '#475569' }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Sorts and deduplicates strings naturally.
|
|
247
|
+
* @param {string[]} values Source values.
|
|
248
|
+
* @returns {string[]}
|
|
249
|
+
*/
|
|
250
|
+
static #sortedStrings(values) {
|
|
251
|
+
return [...new Set((values || []).filter(Boolean))].sort(
|
|
252
|
+
(left, right) =>
|
|
253
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Converts a value to a deterministic lowercase key segment.
|
|
259
|
+
* @param {unknown} value Source value.
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
static #slug(value) {
|
|
263
|
+
return (
|
|
264
|
+
String(value || '')
|
|
265
|
+
.trim()
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
268
|
+
.replace(/^-+|-+$/gu, '') || 'item'
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Rounds numeric values for stable JSON.
|
|
274
|
+
* @param {number} value Candidate number.
|
|
275
|
+
* @returns {number}
|
|
276
|
+
*/
|
|
277
|
+
static #round(value) {
|
|
278
|
+
return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Removes empty fields while preserving zeros and false.
|
|
283
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
284
|
+
* @returns {Record<string, unknown>}
|
|
285
|
+
*/
|
|
286
|
+
static #stripEmpty(value) {
|
|
287
|
+
return Object.fromEntries(
|
|
288
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
289
|
+
if (Array.isArray(entryValue)) return entryValue.length > 0
|
|
290
|
+
return (
|
|
291
|
+
entryValue !== null &&
|
|
292
|
+
entryValue !== undefined &&
|
|
293
|
+
entryValue !== ''
|
|
294
|
+
)
|
|
295
|
+
})
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derives a rigid-flex topology report from the PCB layer-stack read model.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbRigidFlexTopologyBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.rigid-flex-topology.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a rigid-flex topology sidecar.
|
|
13
|
+
* @param {object | undefined} layerStackReadModel Layer-stack sidecar.
|
|
14
|
+
* @returns {object | undefined}
|
|
15
|
+
*/
|
|
16
|
+
static build(layerStackReadModel) {
|
|
17
|
+
if (!layerStackReadModel) return undefined
|
|
18
|
+
|
|
19
|
+
const substacks = layerStackReadModel.substacks || []
|
|
20
|
+
const boardRegions = layerStackReadModel.boardRegions || []
|
|
21
|
+
const substackRegionJoins =
|
|
22
|
+
PcbRigidFlexTopologyBuilder.#substackRegionJoins(substacks)
|
|
23
|
+
const branchGraph = PcbRigidFlexTopologyBuilder.#branchGraph(
|
|
24
|
+
layerStackReadModel.branches || [],
|
|
25
|
+
substacks
|
|
26
|
+
)
|
|
27
|
+
const bendLines = PcbRigidFlexTopologyBuilder.#bendLines(
|
|
28
|
+
substacks,
|
|
29
|
+
boardRegions
|
|
30
|
+
)
|
|
31
|
+
const diagnostics = PcbRigidFlexTopologyBuilder.#diagnostics({
|
|
32
|
+
substacks,
|
|
33
|
+
branchGraph
|
|
34
|
+
})
|
|
35
|
+
const summary = {
|
|
36
|
+
substackCount: substacks.length,
|
|
37
|
+
flexSubstackCount: substacks.filter((substack) => substack.isFlex)
|
|
38
|
+
.length,
|
|
39
|
+
boardRegionCount:
|
|
40
|
+
layerStackReadModel.summary?.boardRegionCount || 0,
|
|
41
|
+
branchCount: branchGraph.length,
|
|
42
|
+
bendLineCount: bendLines.length,
|
|
43
|
+
diagnosticCount: diagnostics.length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
schema: PcbRigidFlexTopologyBuilder.SCHEMA_ID,
|
|
48
|
+
summary,
|
|
49
|
+
substackRegionJoins,
|
|
50
|
+
branchGraph,
|
|
51
|
+
bendLines,
|
|
52
|
+
diagnostics
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds substack-to-board-region join rows.
|
|
58
|
+
* @param {object[]} substacks Layer substacks.
|
|
59
|
+
* @returns {object[]}
|
|
60
|
+
*/
|
|
61
|
+
static #substackRegionJoins(substacks) {
|
|
62
|
+
return substacks.map((substack) => ({
|
|
63
|
+
substackId: substack.id,
|
|
64
|
+
substackName: substack.name,
|
|
65
|
+
isFlex: substack.isFlex,
|
|
66
|
+
layerKeys: substack.layerKeys || [],
|
|
67
|
+
regionIndexes: substack.boardRegionIndexes || [],
|
|
68
|
+
regionNames: substack.boardRegionNames || []
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds a branch graph with resolved child substack summaries.
|
|
74
|
+
* @param {object[]} branches Stack branches.
|
|
75
|
+
* @param {object[]} substacks Layer substacks.
|
|
76
|
+
* @returns {object[]}
|
|
77
|
+
*/
|
|
78
|
+
static #branchGraph(branches, substacks) {
|
|
79
|
+
const substackById = new Map(
|
|
80
|
+
substacks
|
|
81
|
+
.filter((substack) => substack.id)
|
|
82
|
+
.map((substack) => [substack.id, substack])
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return branches.map((branch) => ({
|
|
86
|
+
branchId: branch.id,
|
|
87
|
+
branchName: branch.name,
|
|
88
|
+
rootStackRef: branch.rootStackRef,
|
|
89
|
+
stackRefs: branch.stackRefs || [],
|
|
90
|
+
...(branch.sections ? { sections: branch.sections } : {}),
|
|
91
|
+
childSubstacks: (branch.stackRefs || []).flatMap((stackRef) => {
|
|
92
|
+
const substack = substackById.get(stackRef)
|
|
93
|
+
if (!substack) return []
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
id: substack.id,
|
|
98
|
+
name: substack.name,
|
|
99
|
+
isFlex: substack.isFlex
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
})
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds bend-line summaries.
|
|
108
|
+
* @param {object[]} substacks Layer substacks.
|
|
109
|
+
* @param {object[]} boardRegions Board-region summaries.
|
|
110
|
+
* @returns {object[]}
|
|
111
|
+
*/
|
|
112
|
+
static #bendLines(substacks, boardRegions) {
|
|
113
|
+
const regionNameByIndex = new Map(
|
|
114
|
+
boardRegions.map((region) => [region.regionIndex, region.name])
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return substacks.flatMap((substack) =>
|
|
118
|
+
(substack.boardRegionIndexes || []).flatMap((regionIndex) =>
|
|
119
|
+
Array.from({ length: substack.bendingLineCount || 0 }).map(
|
|
120
|
+
(_, lineIndex) =>
|
|
121
|
+
PcbRigidFlexTopologyBuilder.#stripUndefined({
|
|
122
|
+
substackId: substack.id,
|
|
123
|
+
substackName: substack.name,
|
|
124
|
+
regionIndex,
|
|
125
|
+
regionName: regionNameByIndex.get(regionIndex),
|
|
126
|
+
lineIndex
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds topology diagnostics.
|
|
135
|
+
* @param {{ substacks: object[], branchGraph: object[] }} input Topology sections.
|
|
136
|
+
* @returns {object[]}
|
|
137
|
+
*/
|
|
138
|
+
static #diagnostics(input) {
|
|
139
|
+
const diagnostics = []
|
|
140
|
+
const substackIds = new Set(
|
|
141
|
+
input.substacks.map((substack) => substack.id).filter(Boolean)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
for (const branch of input.branchGraph) {
|
|
145
|
+
for (const stackRef of branch.stackRefs || []) {
|
|
146
|
+
if (substackIds.has(stackRef)) continue
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
code: 'pcb.rigid-flex.unresolved-branch-substack',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
message:
|
|
151
|
+
'Rigid-flex branch graph references an unknown substack.',
|
|
152
|
+
branchId: branch.branchId,
|
|
153
|
+
stackRef
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return diagnostics
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Removes undefined values from an object.
|
|
163
|
+
* @param {Record<string, unknown>} object Source object.
|
|
164
|
+
* @returns {object}
|
|
165
|
+
*/
|
|
166
|
+
static #stripUndefined(object) {
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
Object.entries(object).filter(([, value]) => value !== undefined)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|