altium-toolkit 1.0.8 → 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/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- 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,499 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic netlist exports from normalized project bundles.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectNetlistExporter {
|
|
9
|
+
/**
|
|
10
|
+
* Builds a line-oriented wirelist for CI and downstream tooling.
|
|
11
|
+
* @param {object} bundle Normalized design bundle or effective variant.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
static buildWirelist(bundle) {
|
|
15
|
+
const netlist = ProjectNetlistExporter.buildNetlistJson(bundle)
|
|
16
|
+
const lines = [
|
|
17
|
+
'# altium-toolkit wirelist v1',
|
|
18
|
+
'project ' + netlist.project
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for (const net of netlist.nets) {
|
|
22
|
+
lines.push('net ' + net.name)
|
|
23
|
+
for (const pin of net.pins) {
|
|
24
|
+
lines.push(' ' + pin.component + '.' + pin.pin)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
lines.push('')
|
|
29
|
+
return lines.join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds a deterministic JSON netlist contract.
|
|
34
|
+
* @param {object} bundle Normalized design bundle or effective variant.
|
|
35
|
+
* @returns {{ schema: string, project: string, nets: object[] }}
|
|
36
|
+
*/
|
|
37
|
+
static buildNetlistJson(bundle) {
|
|
38
|
+
const projectName =
|
|
39
|
+
bundle?.project?.name ||
|
|
40
|
+
bundle?.projectName ||
|
|
41
|
+
bundle?.name ||
|
|
42
|
+
bundle?.summary?.title ||
|
|
43
|
+
''
|
|
44
|
+
const nets = (bundle?.nets || [])
|
|
45
|
+
.map((net) => ({
|
|
46
|
+
name: String(net?.name || ''),
|
|
47
|
+
aliases: ProjectNetlistExporter.#netAliases(net),
|
|
48
|
+
autoNamed: ProjectNetlistExporter.#isAutoNamedNet(net?.name),
|
|
49
|
+
signal: ProjectNetlistExporter.#signalDescriptor(net),
|
|
50
|
+
pins: ProjectNetlistExporter.#netPins(net),
|
|
51
|
+
sources: ProjectNetlistExporter.#netSources(net),
|
|
52
|
+
pcb: ProjectNetlistExporter.#pcbSources(net)
|
|
53
|
+
}))
|
|
54
|
+
.filter((net) => net.name)
|
|
55
|
+
.sort((left, right) =>
|
|
56
|
+
left.name.localeCompare(right.name, undefined, {
|
|
57
|
+
numeric: true
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
schema: 'altium-toolkit.netlist.a1',
|
|
63
|
+
project: projectName,
|
|
64
|
+
nets
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extracts deterministic pins from one normalized net row.
|
|
70
|
+
* @param {object} net Net row.
|
|
71
|
+
* @returns {object[]}
|
|
72
|
+
*/
|
|
73
|
+
static #netPins(net) {
|
|
74
|
+
const endpointsByPin = ProjectNetlistExporter.#pinEndpointMap(net)
|
|
75
|
+
const pins = (net?.pins || [])
|
|
76
|
+
.map((pin) =>
|
|
77
|
+
ProjectNetlistExporter.#pinDescriptor(pin, endpointsByPin)
|
|
78
|
+
)
|
|
79
|
+
.filter((pin) => pin.component && pin.pin)
|
|
80
|
+
|
|
81
|
+
return ProjectNetlistExporter.#dedupePins(pins).sort((left, right) => {
|
|
82
|
+
const componentOrder = left.component.localeCompare(
|
|
83
|
+
right.component,
|
|
84
|
+
undefined,
|
|
85
|
+
{ numeric: true }
|
|
86
|
+
)
|
|
87
|
+
return (
|
|
88
|
+
componentOrder ||
|
|
89
|
+
left.pin.localeCompare(right.pin, undefined, { numeric: true })
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deduplicates pins while preserving first-seen data.
|
|
96
|
+
* @param {object[]} pins Candidate pins.
|
|
97
|
+
* @returns {object[]}
|
|
98
|
+
*/
|
|
99
|
+
static #dedupePins(pins) {
|
|
100
|
+
const byKey = new Map()
|
|
101
|
+
for (const pin of pins || []) {
|
|
102
|
+
const key = pin.component + '\u0000' + pin.pin
|
|
103
|
+
if (!byKey.has(key)) {
|
|
104
|
+
byKey.set(key, pin)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const existing = byKey.get(key)
|
|
109
|
+
existing.duplicateOccurrences ||= []
|
|
110
|
+
existing.duplicateOccurrences.push(
|
|
111
|
+
ProjectNetlistExporter.#duplicatePinDescriptor(pin)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return [...byKey.values()]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds one terminal descriptor from a normalized pin row.
|
|
119
|
+
* @param {object} pin Pin row.
|
|
120
|
+
* @param {Map<string, object[]>} endpointsByPin Endpoint lookup.
|
|
121
|
+
* @returns {object}
|
|
122
|
+
*/
|
|
123
|
+
static #pinDescriptor(pin, endpointsByPin) {
|
|
124
|
+
const component = ProjectNetlistExporter.#pinComponent(pin)
|
|
125
|
+
const pinNumber = ProjectNetlistExporter.#pinNumber(pin)
|
|
126
|
+
const endpoints =
|
|
127
|
+
endpointsByPin.get(
|
|
128
|
+
ProjectNetlistExporter.#pinKey(component, pinNumber)
|
|
129
|
+
) || []
|
|
130
|
+
const hierarchyPath = ProjectNetlistExporter.#dedupe(
|
|
131
|
+
endpoints.flatMap((endpoint) => endpoint.hierarchyPath || [])
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return ProjectNetlistExporter.#stripEmpty({
|
|
135
|
+
component,
|
|
136
|
+
pin: pinNumber,
|
|
137
|
+
name: pin?.name && pin.name !== pinNumber ? String(pin.name) : '',
|
|
138
|
+
hierarchyPath,
|
|
139
|
+
ownerPartId: ProjectNetlistExporter.#ownerPartId(pin),
|
|
140
|
+
partUniqueId: ProjectNetlistExporter.#partUniqueId(pin),
|
|
141
|
+
isMultiPart: pin?.isMultiPart === true ? true : undefined,
|
|
142
|
+
alternatePartSuffix:
|
|
143
|
+
ProjectNetlistExporter.#alternatePartSuffix(pin),
|
|
144
|
+
endpoints: endpoints.map(
|
|
145
|
+
({ hierarchyPath: _hierarchyPath, ...endpoint }) => endpoint
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds compact duplicate-pin provenance.
|
|
152
|
+
* @param {object} pin Pin descriptor.
|
|
153
|
+
* @returns {object}
|
|
154
|
+
*/
|
|
155
|
+
static #duplicatePinDescriptor(pin) {
|
|
156
|
+
return ProjectNetlistExporter.#stripEmpty({
|
|
157
|
+
component: pin.component,
|
|
158
|
+
pin: pin.pin,
|
|
159
|
+
name: pin.name,
|
|
160
|
+
ownerPartId: pin.ownerPartId,
|
|
161
|
+
partUniqueId: pin.partUniqueId,
|
|
162
|
+
isMultiPart: pin.isMultiPart,
|
|
163
|
+
alternatePartSuffix: pin.alternatePartSuffix
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Builds schematic endpoint lookup rows for all pins in one net.
|
|
169
|
+
* @param {object} net Net row.
|
|
170
|
+
* @returns {Map<string, object[]>}
|
|
171
|
+
*/
|
|
172
|
+
static #pinEndpointMap(net) {
|
|
173
|
+
const endpointsByPin = new Map()
|
|
174
|
+
|
|
175
|
+
for (const source of net?.schematic || []) {
|
|
176
|
+
const sheet = String(source?.fileName || '')
|
|
177
|
+
const hierarchyPath = ProjectNetlistExporter.#hierarchyPath(source)
|
|
178
|
+
for (const pin of source?.pins || []) {
|
|
179
|
+
const component = ProjectNetlistExporter.#pinComponent(pin)
|
|
180
|
+
const pinNumber = ProjectNetlistExporter.#pinNumber(pin)
|
|
181
|
+
if (!component || !pinNumber) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const key = ProjectNetlistExporter.#pinKey(component, pinNumber)
|
|
186
|
+
if (!endpointsByPin.has(key)) {
|
|
187
|
+
endpointsByPin.set(key, [])
|
|
188
|
+
}
|
|
189
|
+
endpointsByPin.get(key).push({
|
|
190
|
+
kind: 'schematic-pin',
|
|
191
|
+
key: sheet + ':pin:' + component + '.' + pinNumber,
|
|
192
|
+
sheet,
|
|
193
|
+
component,
|
|
194
|
+
pin: pinNumber,
|
|
195
|
+
hierarchyPath
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return endpointsByPin
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Builds graphical source groups for one net.
|
|
205
|
+
* @param {object} net Net row.
|
|
206
|
+
* @returns {object[]}
|
|
207
|
+
*/
|
|
208
|
+
static #netSources(net) {
|
|
209
|
+
return (net?.schematic || []).map((source) => {
|
|
210
|
+
const sheet = String(source?.fileName || '')
|
|
211
|
+
|
|
212
|
+
return ProjectNetlistExporter.#stripEmpty({
|
|
213
|
+
sheet,
|
|
214
|
+
hierarchyPath: ProjectNetlistExporter.#hierarchyPath(source),
|
|
215
|
+
aliases: ProjectNetlistExporter.#sourceAliases(source),
|
|
216
|
+
graphicalElements: ProjectNetlistExporter.#graphicalElements(
|
|
217
|
+
source,
|
|
218
|
+
sheet
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Builds graphical element rows for one schematic net source.
|
|
226
|
+
* @param {object} source Schematic net source.
|
|
227
|
+
* @param {string} sheet Sheet file name.
|
|
228
|
+
* @returns {object[]}
|
|
229
|
+
*/
|
|
230
|
+
static #graphicalElements(source, sheet) {
|
|
231
|
+
return [
|
|
232
|
+
...(source?.segments || []).map((segment, index) =>
|
|
233
|
+
ProjectNetlistExporter.#stripEmpty({
|
|
234
|
+
kind: 'segment',
|
|
235
|
+
key: sheet + ':segment:' + index,
|
|
236
|
+
x1: segment.x1,
|
|
237
|
+
y1: segment.y1,
|
|
238
|
+
x2: segment.x2,
|
|
239
|
+
y2: segment.y2
|
|
240
|
+
})
|
|
241
|
+
),
|
|
242
|
+
...(source?.labels || []).map((label, index) =>
|
|
243
|
+
ProjectNetlistExporter.#stripEmpty({
|
|
244
|
+
kind: 'label',
|
|
245
|
+
key: sheet + ':label:' + index,
|
|
246
|
+
text: label.text,
|
|
247
|
+
x: label.x,
|
|
248
|
+
y: label.y
|
|
249
|
+
})
|
|
250
|
+
),
|
|
251
|
+
...(source?.ports || []).map((port, index) =>
|
|
252
|
+
ProjectNetlistExporter.#stripEmpty({
|
|
253
|
+
kind: 'port',
|
|
254
|
+
key: sheet + ':port:' + index,
|
|
255
|
+
text: port.name,
|
|
256
|
+
x: port.x,
|
|
257
|
+
y: port.y
|
|
258
|
+
})
|
|
259
|
+
),
|
|
260
|
+
...(source?.sheetEntries || []).map((entry, index) =>
|
|
261
|
+
ProjectNetlistExporter.#stripEmpty({
|
|
262
|
+
kind: 'sheet-entry',
|
|
263
|
+
key: sheet + ':sheet-entry:' + index,
|
|
264
|
+
text: entry.name,
|
|
265
|
+
x: entry.x,
|
|
266
|
+
y: entry.y
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extracts PCB net provenance rows.
|
|
274
|
+
* @param {object} net Net row.
|
|
275
|
+
* @returns {object[]}
|
|
276
|
+
*/
|
|
277
|
+
static #pcbSources(net) {
|
|
278
|
+
return (net?.pcb || []).map((entry) =>
|
|
279
|
+
ProjectNetlistExporter.#stripEmpty({
|
|
280
|
+
fileName: entry.fileName,
|
|
281
|
+
netIndex: entry.netIndex,
|
|
282
|
+
uniqueId: entry.uniqueId
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Collects known aliases for one net.
|
|
289
|
+
* @param {object} net Net row.
|
|
290
|
+
* @returns {string[]}
|
|
291
|
+
*/
|
|
292
|
+
static #netAliases(net) {
|
|
293
|
+
return ProjectNetlistExporter.#dedupe(
|
|
294
|
+
(net?.schematic || []).flatMap((source) =>
|
|
295
|
+
ProjectNetlistExporter.#sourceAliases(source)
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Collects aliases visible in one schematic net source.
|
|
302
|
+
* @param {object} source Schematic net source.
|
|
303
|
+
* @returns {string[]}
|
|
304
|
+
*/
|
|
305
|
+
static #sourceAliases(source) {
|
|
306
|
+
return ProjectNetlistExporter.#dedupe([
|
|
307
|
+
...(source?.labels || []).map((label) => label.text),
|
|
308
|
+
...(source?.powerPorts || []).map((port) => port.text),
|
|
309
|
+
...(source?.ports || []).map((port) => port.name),
|
|
310
|
+
...(source?.sheetEntries || []).map((entry) => entry.name)
|
|
311
|
+
])
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resolves a source hierarchy path.
|
|
316
|
+
* @param {object} source Schematic source row.
|
|
317
|
+
* @returns {string[]}
|
|
318
|
+
*/
|
|
319
|
+
static #hierarchyPath(source) {
|
|
320
|
+
if (
|
|
321
|
+
Array.isArray(source?.hierarchyPath) &&
|
|
322
|
+
source.hierarchyPath.length
|
|
323
|
+
) {
|
|
324
|
+
return source.hierarchyPath.map((part) => String(part))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return source?.fileName ? [String(source.fileName)] : []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Builds signal shape metadata for one net.
|
|
332
|
+
* @param {object} net Net row.
|
|
333
|
+
* @returns {{ type: string, baseName: string, suffix: string, sourceHints: string[] }}
|
|
334
|
+
*/
|
|
335
|
+
static #signalDescriptor(net) {
|
|
336
|
+
const name = String(net?.name || '')
|
|
337
|
+
const bracket = name.match(/^(.+?)(\[[^\]]+\])$/u)
|
|
338
|
+
const sourceHints = ProjectNetlistExporter.#signalSourceHints(net)
|
|
339
|
+
const baseName = bracket ? bracket[1] : name
|
|
340
|
+
const suffix = bracket ? bracket[2] : ''
|
|
341
|
+
let type = 'normal'
|
|
342
|
+
|
|
343
|
+
if (sourceHints.includes('harness')) {
|
|
344
|
+
type = 'harness'
|
|
345
|
+
} else if (suffix && /(?:\.\.|:|,)/u.test(suffix)) {
|
|
346
|
+
type = 'wide'
|
|
347
|
+
} else if (suffix) {
|
|
348
|
+
type = 'sub'
|
|
349
|
+
} else if (sourceHints.includes('bus')) {
|
|
350
|
+
type = 'bus'
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
type,
|
|
355
|
+
baseName,
|
|
356
|
+
suffix,
|
|
357
|
+
sourceHints
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Collects source-derived signal hints.
|
|
363
|
+
* @param {object} net Net row.
|
|
364
|
+
* @returns {string[]}
|
|
365
|
+
*/
|
|
366
|
+
static #signalSourceHints(net) {
|
|
367
|
+
const hints = []
|
|
368
|
+
const schematicSources = net?.schematic || []
|
|
369
|
+
const hasHarness = schematicSources.some(
|
|
370
|
+
(source) =>
|
|
371
|
+
(source?.harnesses || []).length ||
|
|
372
|
+
(source?.sheetEntries || []).some(
|
|
373
|
+
(entry) => entry?.harnessType
|
|
374
|
+
) ||
|
|
375
|
+
(source?.ports || []).some((port) => port?.harnessType)
|
|
376
|
+
)
|
|
377
|
+
const hasBus = schematicSources.some(
|
|
378
|
+
(source) =>
|
|
379
|
+
(source?.busEntries || []).length ||
|
|
380
|
+
(source?.segments || []).some(
|
|
381
|
+
(segment) => segment?.isBus === true
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if (hasHarness) hints.push('harness')
|
|
386
|
+
if (hasBus) hints.push('bus')
|
|
387
|
+
|
|
388
|
+
return hints
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Resolves native owner-part provenance from a pin row.
|
|
393
|
+
* @param {object} pin Pin row.
|
|
394
|
+
* @returns {string}
|
|
395
|
+
*/
|
|
396
|
+
static #ownerPartId(pin) {
|
|
397
|
+
return String(
|
|
398
|
+
pin?.ownerPartId ||
|
|
399
|
+
pin?.ownerPartUniqueId ||
|
|
400
|
+
pin?.ownerPartUid ||
|
|
401
|
+
''
|
|
402
|
+
).trim()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Resolves the native part unique id from a pin row.
|
|
407
|
+
* @param {object} pin Pin row.
|
|
408
|
+
* @returns {string}
|
|
409
|
+
*/
|
|
410
|
+
static #partUniqueId(pin) {
|
|
411
|
+
return String(pin?.partUniqueId || pin?.partUid || '').trim()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns an alternate-part suffix from a variant-qualified part id.
|
|
416
|
+
* @param {object} pin Pin row.
|
|
417
|
+
* @returns {string}
|
|
418
|
+
*/
|
|
419
|
+
static #alternatePartSuffix(pin) {
|
|
420
|
+
const partUniqueId = ProjectNetlistExporter.#partUniqueId(pin)
|
|
421
|
+
return partUniqueId.includes('@')
|
|
422
|
+
? partUniqueId.split('@').slice(1).join('@')
|
|
423
|
+
: ''
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Resolves a component designator from a pin row.
|
|
428
|
+
* @param {object} pin Pin row.
|
|
429
|
+
* @returns {string}
|
|
430
|
+
*/
|
|
431
|
+
static #pinComponent(pin) {
|
|
432
|
+
return String(pin?.componentDesignator || pin?.ownerIndex || '').trim()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Resolves a pin number from a pin row.
|
|
437
|
+
* @param {object} pin Pin row.
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
static #pinNumber(pin) {
|
|
441
|
+
return String(
|
|
442
|
+
pin?.pin || pin?.designator || pin?.number || pin?.name || ''
|
|
443
|
+
).trim()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Builds a stable pin lookup key.
|
|
448
|
+
* @param {string} component Component designator.
|
|
449
|
+
* @param {string} pin Pin number.
|
|
450
|
+
* @returns {string}
|
|
451
|
+
*/
|
|
452
|
+
static #pinKey(component, pin) {
|
|
453
|
+
return component + '\u0000' + pin
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Returns true when a net name was synthesized.
|
|
458
|
+
* @param {unknown} name Net name.
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
static #isAutoNamedNet(name) {
|
|
462
|
+
return /^UnknownNet\d+$/u.test(String(name || ''))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Deduplicates non-empty strings.
|
|
467
|
+
* @param {unknown[]} values Candidate values.
|
|
468
|
+
* @returns {string[]}
|
|
469
|
+
*/
|
|
470
|
+
static #dedupe(values) {
|
|
471
|
+
return [
|
|
472
|
+
...new Set(
|
|
473
|
+
(values || [])
|
|
474
|
+
.map((value) => String(value || '').trim())
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
)
|
|
477
|
+
]
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Drops empty object fields while preserving zero and false.
|
|
482
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
483
|
+
* @returns {Record<string, unknown>}
|
|
484
|
+
*/
|
|
485
|
+
static #stripEmpty(value) {
|
|
486
|
+
return Object.fromEntries(
|
|
487
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
488
|
+
if (Array.isArray(entryValue)) {
|
|
489
|
+
return entryValue.length > 0
|
|
490
|
+
}
|
|
491
|
+
return (
|
|
492
|
+
entryValue !== null &&
|
|
493
|
+
entryValue !== undefined &&
|
|
494
|
+
entryValue !== ''
|
|
495
|
+
)
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a read-only digest of project output-job documents and output rows.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectOutJobDigestBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.project.outjob-digest.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds an output-job digest from normalized project fragments.
|
|
13
|
+
* @param {{ documents?: object[], outputGroups?: object[] }} project Project fragments.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(project) {
|
|
17
|
+
const documents = (project?.documents || [])
|
|
18
|
+
.filter((document) => document.kind === 'output-job')
|
|
19
|
+
.map((document) => ({
|
|
20
|
+
documentIndex: document.index,
|
|
21
|
+
path: document.path,
|
|
22
|
+
normalizedPath: document.normalizedPath,
|
|
23
|
+
fileName: document.fileName
|
|
24
|
+
}))
|
|
25
|
+
const outputGroups = (project?.outputGroups || []).map((group) =>
|
|
26
|
+
ProjectOutJobDigestBuilder.#outputGroup(group)
|
|
27
|
+
)
|
|
28
|
+
const outputCount = outputGroups.reduce(
|
|
29
|
+
(sum, group) => sum + group.outputCount,
|
|
30
|
+
0
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
schema: ProjectOutJobDigestBuilder.SCHEMA_ID,
|
|
35
|
+
summary: {
|
|
36
|
+
outJobDocumentCount: documents.length,
|
|
37
|
+
outputGroupCount: outputGroups.length,
|
|
38
|
+
outputCount
|
|
39
|
+
},
|
|
40
|
+
documents,
|
|
41
|
+
outputGroups,
|
|
42
|
+
outputsByDocumentPath:
|
|
43
|
+
ProjectOutJobDigestBuilder.#outputsByDocumentPath(outputGroups)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes one output group.
|
|
49
|
+
* @param {object} group Project output group.
|
|
50
|
+
* @returns {object}
|
|
51
|
+
*/
|
|
52
|
+
static #outputGroup(group) {
|
|
53
|
+
const outputs = (group.outputs || []).map((output) => ({
|
|
54
|
+
index: output.index,
|
|
55
|
+
type: output.type,
|
|
56
|
+
name: output.name,
|
|
57
|
+
documentPath: output.documentPath,
|
|
58
|
+
normalizedDocumentPath: ProjectOutJobDigestBuilder.#normalizePath(
|
|
59
|
+
output.documentPath
|
|
60
|
+
),
|
|
61
|
+
variantName: output.variantName,
|
|
62
|
+
isDefault: output.isDefault
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
index: group.index,
|
|
67
|
+
name: group.name,
|
|
68
|
+
outputCount: outputs.length,
|
|
69
|
+
outputs
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builds a lookup keyed by normalized output document path.
|
|
75
|
+
* @param {object[]} outputGroups Normalized output groups.
|
|
76
|
+
* @returns {Record<string, object[]>}
|
|
77
|
+
*/
|
|
78
|
+
static #outputsByDocumentPath(outputGroups) {
|
|
79
|
+
const byPath = {}
|
|
80
|
+
|
|
81
|
+
for (const group of outputGroups) {
|
|
82
|
+
for (const output of group.outputs) {
|
|
83
|
+
const path = output.normalizedDocumentPath
|
|
84
|
+
if (!path) continue
|
|
85
|
+
byPath[path] ||= []
|
|
86
|
+
byPath[path].push({
|
|
87
|
+
outputGroupName: group.name,
|
|
88
|
+
outputGroupIndex: group.index,
|
|
89
|
+
outputIndex: output.index,
|
|
90
|
+
type: output.type,
|
|
91
|
+
name: output.name,
|
|
92
|
+
variantName: output.variantName,
|
|
93
|
+
isDefault: output.isDefault
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return byPath
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalizes project path separators.
|
|
103
|
+
* @param {string} path Raw path.
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
static #normalizePath(path) {
|
|
107
|
+
return String(path || '').replace(/\\/g, '/')
|
|
108
|
+
}
|
|
109
|
+
}
|