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,237 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic BOM/PnP reconciliation metadata for a project bundle.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectBomPnpReconciliationBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.project.bom-pnp-reconciliation.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a reconciliation report from bundle and source document models.
|
|
13
|
+
* @param {{ bundle?: object, documentModels?: object[], effectiveVariant?: object }} options Report options.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(options = {}) {
|
|
17
|
+
const bundle = options.bundle || {}
|
|
18
|
+
const documentModels = Array.isArray(options.documentModels)
|
|
19
|
+
? options.documentModels
|
|
20
|
+
: []
|
|
21
|
+
const schematicBomDesignators =
|
|
22
|
+
ProjectBomPnpReconciliationBuilder.#bomDesignators(
|
|
23
|
+
documentModels.filter((model) => model?.kind === 'schematic')
|
|
24
|
+
)
|
|
25
|
+
const pcbBomDesignators =
|
|
26
|
+
ProjectBomPnpReconciliationBuilder.#bomDesignators(
|
|
27
|
+
documentModels.filter((model) => model?.kind === 'pcb')
|
|
28
|
+
)
|
|
29
|
+
const pnpDesignators =
|
|
30
|
+
ProjectBomPnpReconciliationBuilder.#pnpDesignators(documentModels)
|
|
31
|
+
const effectiveBomDesignators =
|
|
32
|
+
ProjectBomPnpReconciliationBuilder.#effectiveBomDesignators(
|
|
33
|
+
bundle,
|
|
34
|
+
options.effectiveVariant
|
|
35
|
+
)
|
|
36
|
+
const noBomDesignators =
|
|
37
|
+
ProjectBomPnpReconciliationBuilder.#noBomDesignators(documentModels)
|
|
38
|
+
const issues = [
|
|
39
|
+
...ProjectBomPnpReconciliationBuilder.#missingIssues(
|
|
40
|
+
schematicBomDesignators,
|
|
41
|
+
pcbBomDesignators,
|
|
42
|
+
'reconciliation.schematic-bom-without-pcb-bom',
|
|
43
|
+
'Schematic BOM designator was not present in the PCB-backed BOM.'
|
|
44
|
+
),
|
|
45
|
+
...ProjectBomPnpReconciliationBuilder.#missingIssues(
|
|
46
|
+
pcbBomDesignators,
|
|
47
|
+
schematicBomDesignators,
|
|
48
|
+
'reconciliation.pcb-bom-without-schematic-bom',
|
|
49
|
+
'PCB-backed BOM designator was not present in the schematic BOM.'
|
|
50
|
+
),
|
|
51
|
+
...ProjectBomPnpReconciliationBuilder.#missingIssues(
|
|
52
|
+
pcbBomDesignators,
|
|
53
|
+
pnpDesignators,
|
|
54
|
+
'reconciliation.bom-without-pnp',
|
|
55
|
+
'PCB-backed BOM designator did not have a PnP placement.'
|
|
56
|
+
),
|
|
57
|
+
...ProjectBomPnpReconciliationBuilder.#missingIssues(
|
|
58
|
+
pnpDesignators,
|
|
59
|
+
pcbBomDesignators,
|
|
60
|
+
'reconciliation.pnp-without-bom',
|
|
61
|
+
'PnP placement designator was not present in the PCB-backed BOM.'
|
|
62
|
+
),
|
|
63
|
+
...ProjectBomPnpReconciliationBuilder.#intersectionIssues(
|
|
64
|
+
noBomDesignators,
|
|
65
|
+
pcbBomDesignators,
|
|
66
|
+
'reconciliation.no-bom-component-in-pcb-bom',
|
|
67
|
+
'Component marked as no-BOM appeared in the PCB-backed BOM.'
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
schema: ProjectBomPnpReconciliationBuilder.SCHEMA_ID,
|
|
73
|
+
summary: {
|
|
74
|
+
schematicBomDesignatorCount: schematicBomDesignators.length,
|
|
75
|
+
pcbBomDesignatorCount: pcbBomDesignators.length,
|
|
76
|
+
pnpDesignatorCount: pnpDesignators.length,
|
|
77
|
+
effectiveBomDesignatorCount: effectiveBomDesignators.length,
|
|
78
|
+
noBomComponentCount: noBomDesignators.length,
|
|
79
|
+
issueCount: issues.length
|
|
80
|
+
},
|
|
81
|
+
schematicBomDesignators,
|
|
82
|
+
pcbBomDesignators,
|
|
83
|
+
pnpDesignators,
|
|
84
|
+
effectiveBomDesignators,
|
|
85
|
+
noBomDesignators,
|
|
86
|
+
issues
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extracts designators from BOM rows.
|
|
92
|
+
* @param {object[]} models Parsed document models.
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
static #bomDesignators(models) {
|
|
96
|
+
const designators = new Set()
|
|
97
|
+
|
|
98
|
+
for (const model of models) {
|
|
99
|
+
for (const row of model?.bom || []) {
|
|
100
|
+
for (const designator of row.designators || []) {
|
|
101
|
+
ProjectBomPnpReconciliationBuilder.#addDesignator(
|
|
102
|
+
designators,
|
|
103
|
+
designator
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
ProjectBomPnpReconciliationBuilder.#addDesignator(
|
|
107
|
+
designators,
|
|
108
|
+
row.designator
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ProjectBomPnpReconciliationBuilder.#sorted([...designators])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extracts designators from pick-place entries.
|
|
118
|
+
* @param {object[]} models Parsed document models.
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
static #pnpDesignators(models) {
|
|
122
|
+
const designators = new Set()
|
|
123
|
+
|
|
124
|
+
for (const model of models.filter((item) => item?.kind === 'pcb')) {
|
|
125
|
+
const pnp = model.pnp || model.pcb?.pickPlace || {}
|
|
126
|
+
for (const entry of pnp.entries || []) {
|
|
127
|
+
ProjectBomPnpReconciliationBuilder.#addDesignator(
|
|
128
|
+
designators,
|
|
129
|
+
entry.designator
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ProjectBomPnpReconciliationBuilder.#sorted([...designators])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extracts designators from the active effective variant or bundle BOM.
|
|
139
|
+
* @param {object} bundle Project design bundle.
|
|
140
|
+
* @param {object | undefined} effectiveVariant Effective variant view.
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
static #effectiveBomDesignators(bundle, effectiveVariant) {
|
|
144
|
+
if (effectiveVariant?.bom) {
|
|
145
|
+
return ProjectBomPnpReconciliationBuilder.#bomDesignators([
|
|
146
|
+
{ bom: effectiveVariant.bom }
|
|
147
|
+
])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ProjectBomPnpReconciliationBuilder.#bomDesignators([
|
|
151
|
+
{ bom: bundle.bom || [] }
|
|
152
|
+
])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extracts component designators explicitly excluded from BOMs.
|
|
157
|
+
* @param {object[]} models Parsed document models.
|
|
158
|
+
* @returns {string[]}
|
|
159
|
+
*/
|
|
160
|
+
static #noBomDesignators(models) {
|
|
161
|
+
const designators = new Set()
|
|
162
|
+
|
|
163
|
+
for (const model of models.filter((item) => item?.kind === 'pcb')) {
|
|
164
|
+
for (const component of model.pcb?.components || []) {
|
|
165
|
+
if (component.componentKind?.includeInBom !== false) continue
|
|
166
|
+
ProjectBomPnpReconciliationBuilder.#addDesignator(
|
|
167
|
+
designators,
|
|
168
|
+
component.designator
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return ProjectBomPnpReconciliationBuilder.#sorted([...designators])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Adds a normalized designator to a set.
|
|
178
|
+
* @param {Set<string>} designators Target set.
|
|
179
|
+
* @param {unknown} value Raw designator value.
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
static #addDesignator(designators, value) {
|
|
183
|
+
const designator = String(value || '').trim()
|
|
184
|
+
if (designator) designators.add(designator)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Builds missing-designator issue rows.
|
|
189
|
+
* @param {string[]} source Source designators.
|
|
190
|
+
* @param {string[]} target Target designators.
|
|
191
|
+
* @param {string} code Diagnostic code.
|
|
192
|
+
* @param {string} message Diagnostic message.
|
|
193
|
+
* @returns {object[]}
|
|
194
|
+
*/
|
|
195
|
+
static #missingIssues(source, target, code, message) {
|
|
196
|
+
const targetSet = new Set(target)
|
|
197
|
+
return source
|
|
198
|
+
.filter((designator) => !targetSet.has(designator))
|
|
199
|
+
.map((designator) => ({
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
code,
|
|
202
|
+
designator,
|
|
203
|
+
message
|
|
204
|
+
}))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Builds issue rows for designators present in both sets.
|
|
209
|
+
* @param {string[]} left Left designators.
|
|
210
|
+
* @param {string[]} right Right designators.
|
|
211
|
+
* @param {string} code Diagnostic code.
|
|
212
|
+
* @param {string} message Diagnostic message.
|
|
213
|
+
* @returns {object[]}
|
|
214
|
+
*/
|
|
215
|
+
static #intersectionIssues(left, right, code, message) {
|
|
216
|
+
const rightSet = new Set(right)
|
|
217
|
+
return left
|
|
218
|
+
.filter((designator) => rightSet.has(designator))
|
|
219
|
+
.map((designator) => ({
|
|
220
|
+
severity: 'warning',
|
|
221
|
+
code,
|
|
222
|
+
designator,
|
|
223
|
+
message
|
|
224
|
+
}))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Sorts designators in a stable human-friendly order.
|
|
229
|
+
* @param {string[]} values Designator values.
|
|
230
|
+
* @returns {string[]}
|
|
231
|
+
*/
|
|
232
|
+
static #sorted(values) {
|
|
233
|
+
return [...values].sort((left, right) =>
|
|
234
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ProjectBomPnpReconciliationBuilder } from './ProjectBomPnpReconciliationBuilder.mjs'
|
|
6
7
|
import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,6 +11,19 @@ import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
|
|
|
10
11
|
* bundle for multi-document consumers.
|
|
11
12
|
*/
|
|
12
13
|
export class ProjectDesignBundleBuilder {
|
|
14
|
+
static #UNITS = {
|
|
15
|
+
coordinate: 'mil',
|
|
16
|
+
length: 'mil',
|
|
17
|
+
board: 'mil',
|
|
18
|
+
pnp: 'mil',
|
|
19
|
+
angle: 'deg'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static #PNP_UNITS = {
|
|
23
|
+
coordinate: 'mil',
|
|
24
|
+
angle: 'deg'
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Builds a normalized project/design bundle from already parsed models.
|
|
15
29
|
* @param {{ projectModel?: object, documentModels?: object[], annotationModels?: object[], variantName?: string }} options Bundle options.
|
|
@@ -78,6 +92,7 @@ export class ProjectDesignBundleBuilder {
|
|
|
78
92
|
}
|
|
79
93
|
],
|
|
80
94
|
project,
|
|
95
|
+
units: ProjectDesignBundleBuilder.#UNITS,
|
|
81
96
|
variants: project.variants || [],
|
|
82
97
|
sheets,
|
|
83
98
|
components,
|
|
@@ -100,6 +115,12 @@ export class ProjectDesignBundleBuilder {
|
|
|
100
115
|
})
|
|
101
116
|
}
|
|
102
117
|
|
|
118
|
+
bundle.reconciliation = ProjectBomPnpReconciliationBuilder.build({
|
|
119
|
+
bundle,
|
|
120
|
+
documentModels,
|
|
121
|
+
effectiveVariant: bundle.effectiveVariant
|
|
122
|
+
})
|
|
123
|
+
|
|
103
124
|
return bundle
|
|
104
125
|
}
|
|
105
126
|
|
|
@@ -215,6 +236,7 @@ export class ProjectDesignBundleBuilder {
|
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
return {
|
|
239
|
+
units: ProjectDesignBundleBuilder.#PNP_UNITS,
|
|
218
240
|
positionMode,
|
|
219
241
|
entries,
|
|
220
242
|
modes: {}
|
|
@@ -294,15 +316,67 @@ export class ProjectDesignBundleBuilder {
|
|
|
294
316
|
* @returns {object[]}
|
|
295
317
|
*/
|
|
296
318
|
static #buildBom(documentModels) {
|
|
319
|
+
const noBomDesignators =
|
|
320
|
+
ProjectDesignBundleBuilder.#noBomDesignators(documentModels)
|
|
297
321
|
const pcbBom = documentModels
|
|
298
322
|
.filter((model) => model?.kind === 'pcb')
|
|
299
323
|
.flatMap((model) => model.bom || [])
|
|
300
324
|
|
|
301
325
|
if (pcbBom.length) {
|
|
302
|
-
return
|
|
326
|
+
return ProjectDesignBundleBuilder.#filterBomRows(
|
|
327
|
+
pcbBom,
|
|
328
|
+
noBomDesignators
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return ProjectDesignBundleBuilder.#filterBomRows(
|
|
333
|
+
documentModels.flatMap((model) => model.bom || []),
|
|
334
|
+
noBomDesignators
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Removes component-kind no-BOM designators from normalized BOM rows.
|
|
340
|
+
* @param {object[]} rows BOM rows.
|
|
341
|
+
* @param {Set<string>} noBomDesignators Designators excluded from BOMs.
|
|
342
|
+
* @returns {object[]}
|
|
343
|
+
*/
|
|
344
|
+
static #filterBomRows(rows, noBomDesignators) {
|
|
345
|
+
if (!noBomDesignators.size) return rows
|
|
346
|
+
|
|
347
|
+
return (rows || [])
|
|
348
|
+
.map((row) => {
|
|
349
|
+
const designators = (row.designators || []).filter(
|
|
350
|
+
(designator) => !noBomDesignators.has(designator)
|
|
351
|
+
)
|
|
352
|
+
return {
|
|
353
|
+
...row,
|
|
354
|
+
designators,
|
|
355
|
+
quantity: designators.length || row.quantity
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.filter((row) => row.designators.length > 0)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Collects PCB components whose native kind excludes BOM output.
|
|
363
|
+
* @param {object[]} documentModels Parsed document models.
|
|
364
|
+
* @returns {Set<string>}
|
|
365
|
+
*/
|
|
366
|
+
static #noBomDesignators(documentModels) {
|
|
367
|
+
const designators = new Set()
|
|
368
|
+
|
|
369
|
+
for (const model of documentModels.filter(
|
|
370
|
+
(item) => item?.kind === 'pcb'
|
|
371
|
+
)) {
|
|
372
|
+
for (const component of model.pcb?.components || []) {
|
|
373
|
+
if (component.componentKind?.includeInBom !== false) continue
|
|
374
|
+
const designator = String(component.designator || '').trim()
|
|
375
|
+
if (designator) designators.add(designator)
|
|
376
|
+
}
|
|
303
377
|
}
|
|
304
378
|
|
|
305
|
-
return
|
|
379
|
+
return designators
|
|
306
380
|
}
|
|
307
381
|
|
|
308
382
|
/**
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a read-only project document graph from parsed PrjPcb metadata.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectDocumentGraphBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.project.document-graph.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a normalized document graph index.
|
|
13
|
+
* @param {object} projectModel Parsed project model or project payload.
|
|
14
|
+
* @param {{ availablePaths?: string[] | Set<string> }} options Graph options.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static build(projectModel = {}, options = {}) {
|
|
18
|
+
const project = projectModel?.project || projectModel || {}
|
|
19
|
+
const documents = ProjectDocumentGraphBuilder.#documentRows(
|
|
20
|
+
project.documents || [],
|
|
21
|
+
project.outputGroups || [],
|
|
22
|
+
options
|
|
23
|
+
)
|
|
24
|
+
const groups = ProjectDocumentGraphBuilder.#groups(
|
|
25
|
+
documents,
|
|
26
|
+
project.outputGroups || []
|
|
27
|
+
)
|
|
28
|
+
const indexes = ProjectDocumentGraphBuilder.#indexes(
|
|
29
|
+
documents,
|
|
30
|
+
project.outputGroups || []
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
schema: ProjectDocumentGraphBuilder.SCHEMA,
|
|
35
|
+
summary: {
|
|
36
|
+
documentCount: documents.length,
|
|
37
|
+
sourceSheetCount: groups.sourceSheets.length,
|
|
38
|
+
pcbDocumentCount: groups.pcbs.length,
|
|
39
|
+
linkedLibraryCount: groups.linkedLibraries.length,
|
|
40
|
+
harnessFileCount: groups.harnessFiles.length,
|
|
41
|
+
outJobReferenceCount: groups.outJobs.length,
|
|
42
|
+
generatedOutputCount: groups.generatedOutputs.length,
|
|
43
|
+
missingPathCount: groups.missingPaths.length
|
|
44
|
+
},
|
|
45
|
+
documents,
|
|
46
|
+
groups,
|
|
47
|
+
indexes
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds detailed document graph rows.
|
|
53
|
+
* @param {object[]} documents Project document rows.
|
|
54
|
+
* @param {object[]} outputGroups Project output groups.
|
|
55
|
+
* @param {{ availablePaths?: string[] | Set<string> }} options Graph options.
|
|
56
|
+
* @returns {object[]}
|
|
57
|
+
*/
|
|
58
|
+
static #documentRows(documents, outputGroups, options) {
|
|
59
|
+
const availablePaths =
|
|
60
|
+
options.availablePaths == null
|
|
61
|
+
? null
|
|
62
|
+
: new Set(
|
|
63
|
+
[...options.availablePaths].map((path) =>
|
|
64
|
+
ProjectDocumentGraphBuilder.#normalizePath(path)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
const outputsByDocumentPath =
|
|
68
|
+
ProjectDocumentGraphBuilder.#outputsByDocumentPath(outputGroups)
|
|
69
|
+
|
|
70
|
+
return (documents || []).map((document, index) =>
|
|
71
|
+
ProjectDocumentGraphBuilder.#stripUndefined({
|
|
72
|
+
graphIndex: index,
|
|
73
|
+
documentIndex: document.index,
|
|
74
|
+
section: document.section,
|
|
75
|
+
path: document.path || '',
|
|
76
|
+
normalizedPath:
|
|
77
|
+
document.normalizedPath ||
|
|
78
|
+
ProjectDocumentGraphBuilder.#normalizePath(document.path),
|
|
79
|
+
fileName:
|
|
80
|
+
document.fileName ||
|
|
81
|
+
ProjectDocumentGraphBuilder.#basename(document.path),
|
|
82
|
+
extension: document.extension || '',
|
|
83
|
+
kind: document.kind || 'other',
|
|
84
|
+
uniqueId: document.uniqueId || '',
|
|
85
|
+
isStub: document.isStub === true ? true : undefined,
|
|
86
|
+
exists:
|
|
87
|
+
availablePaths === null
|
|
88
|
+
? undefined
|
|
89
|
+
: availablePaths.has(
|
|
90
|
+
document.normalizedPath ||
|
|
91
|
+
ProjectDocumentGraphBuilder.#normalizePath(
|
|
92
|
+
document.path
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
linkedOutputs:
|
|
96
|
+
outputsByDocumentPath[
|
|
97
|
+
document.normalizedPath ||
|
|
98
|
+
ProjectDocumentGraphBuilder.#normalizePath(
|
|
99
|
+
document.path
|
|
100
|
+
)
|
|
101
|
+
] || []
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Groups document and generated-output paths by public role.
|
|
108
|
+
* @param {object[]} documents Document graph rows.
|
|
109
|
+
* @param {object[]} outputGroups Output groups.
|
|
110
|
+
* @returns {object}
|
|
111
|
+
*/
|
|
112
|
+
static #groups(documents, outputGroups) {
|
|
113
|
+
const pathsForKind = (kind) =>
|
|
114
|
+
documents
|
|
115
|
+
.filter((document) => document.kind === kind)
|
|
116
|
+
.map((document) => document.normalizedPath)
|
|
117
|
+
const libraryKinds = new Set([
|
|
118
|
+
'schematic-library',
|
|
119
|
+
'pcb-library',
|
|
120
|
+
'integrated-library'
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
sourceSheets: pathsForKind('schematic'),
|
|
125
|
+
pcbs: pathsForKind('pcb'),
|
|
126
|
+
linkedLibraries: documents
|
|
127
|
+
.filter((document) => libraryKinds.has(document.kind))
|
|
128
|
+
.map((document) => document.normalizedPath),
|
|
129
|
+
schematicLibraries: pathsForKind('schematic-library'),
|
|
130
|
+
pcbLibraries: pathsForKind('pcb-library'),
|
|
131
|
+
integratedLibraries: pathsForKind('integrated-library'),
|
|
132
|
+
harnessFiles: pathsForKind('harness'),
|
|
133
|
+
outJobs: pathsForKind('output-job'),
|
|
134
|
+
generatedOutputs:
|
|
135
|
+
ProjectDocumentGraphBuilder.#generatedOutputPaths(outputGroups),
|
|
136
|
+
missingPaths: documents
|
|
137
|
+
.filter((document) => document.exists === false)
|
|
138
|
+
.map((document) => document.normalizedPath)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Builds graph lookup indexes.
|
|
144
|
+
* @param {object[]} documents Document graph rows.
|
|
145
|
+
* @param {object[]} outputGroups Output groups.
|
|
146
|
+
* @returns {object}
|
|
147
|
+
*/
|
|
148
|
+
static #indexes(documents, outputGroups) {
|
|
149
|
+
const byPath = {}
|
|
150
|
+
const byKind = {}
|
|
151
|
+
for (const document of documents) {
|
|
152
|
+
byPath[document.normalizedPath] = document.graphIndex
|
|
153
|
+
byKind[document.kind] ||= []
|
|
154
|
+
byKind[document.kind].push(document.normalizedPath)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
byPath,
|
|
159
|
+
byKind,
|
|
160
|
+
outputsByDocumentPath:
|
|
161
|
+
ProjectDocumentGraphBuilder.#outputsByDocumentPath(
|
|
162
|
+
outputGroups
|
|
163
|
+
),
|
|
164
|
+
generatedOutputsByPath:
|
|
165
|
+
ProjectDocumentGraphBuilder.#generatedOutputsByPath(
|
|
166
|
+
outputGroups
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds generated-output descriptors keyed by source document path.
|
|
173
|
+
* @param {object[]} outputGroups Project output groups.
|
|
174
|
+
* @returns {Record<string, object[]>}
|
|
175
|
+
*/
|
|
176
|
+
static #outputsByDocumentPath(outputGroups) {
|
|
177
|
+
const outputsByPath = {}
|
|
178
|
+
for (const outputGroup of outputGroups || []) {
|
|
179
|
+
for (const output of outputGroup.outputs || []) {
|
|
180
|
+
const documentPath = ProjectDocumentGraphBuilder.#normalizePath(
|
|
181
|
+
output.normalizedDocumentPath || output.documentPath
|
|
182
|
+
)
|
|
183
|
+
if (!documentPath) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
outputsByPath[documentPath] ||= []
|
|
188
|
+
outputsByPath[documentPath].push(
|
|
189
|
+
ProjectDocumentGraphBuilder.#stripUndefined({
|
|
190
|
+
outputGroupName: outputGroup.name || '',
|
|
191
|
+
outputGroupIndex: outputGroup.index,
|
|
192
|
+
outputIndex: output.index,
|
|
193
|
+
type: output.type || '',
|
|
194
|
+
name: output.name || '',
|
|
195
|
+
variantName: output.variantName || '',
|
|
196
|
+
targetPath:
|
|
197
|
+
ProjectDocumentGraphBuilder.#normalizePath(
|
|
198
|
+
output.targetPath ||
|
|
199
|
+
output.normalizedTargetPath ||
|
|
200
|
+
''
|
|
201
|
+
) || undefined,
|
|
202
|
+
isDefault: output.isDefault === true ? true : undefined
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return outputsByPath
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Lists generated output target paths.
|
|
213
|
+
* @param {object[]} outputGroups Project output groups.
|
|
214
|
+
* @returns {string[]}
|
|
215
|
+
*/
|
|
216
|
+
static #generatedOutputPaths(outputGroups) {
|
|
217
|
+
const paths = []
|
|
218
|
+
for (const outputs of Object.values(
|
|
219
|
+
ProjectDocumentGraphBuilder.#outputsByDocumentPath(outputGroups)
|
|
220
|
+
)) {
|
|
221
|
+
for (const output of outputs) {
|
|
222
|
+
if (output.targetPath && !paths.includes(output.targetPath)) {
|
|
223
|
+
paths.push(output.targetPath)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return paths
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Builds generated-output descriptors keyed by target path.
|
|
232
|
+
* @param {object[]} outputGroups Project output groups.
|
|
233
|
+
* @returns {Record<string, object>}
|
|
234
|
+
*/
|
|
235
|
+
static #generatedOutputsByPath(outputGroups) {
|
|
236
|
+
const byPath = {}
|
|
237
|
+
for (const [sourcePath, outputs] of Object.entries(
|
|
238
|
+
ProjectDocumentGraphBuilder.#outputsByDocumentPath(outputGroups)
|
|
239
|
+
)) {
|
|
240
|
+
for (const output of outputs) {
|
|
241
|
+
if (!output.targetPath) continue
|
|
242
|
+
byPath[output.targetPath] = {
|
|
243
|
+
sourceDocumentPath: sourcePath,
|
|
244
|
+
...output
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return byPath
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Normalizes project-relative path separators.
|
|
253
|
+
* @param {string} path Project path.
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
static #normalizePath(path) {
|
|
257
|
+
return String(path || '').replace(/\\/g, '/')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Extracts a basename without resolving the path.
|
|
262
|
+
* @param {string} path Project path.
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
static #basename(path) {
|
|
266
|
+
const parts = String(path || '').split(/[\\/]/u)
|
|
267
|
+
return parts[parts.length - 1] || ''
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Removes undefined object properties for stable JSON output.
|
|
272
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
273
|
+
* @returns {Record<string, unknown>}
|
|
274
|
+
*/
|
|
275
|
+
static #stripUndefined(value) {
|
|
276
|
+
return Object.fromEntries(
|
|
277
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -32,7 +32,7 @@ export class ProjectNetlistExporter {
|
|
|
32
32
|
/**
|
|
33
33
|
* Builds a deterministic JSON netlist contract.
|
|
34
34
|
* @param {object} bundle Normalized design bundle or effective variant.
|
|
35
|
-
* @returns {{ schema: string, project: string, nets: object[] }}
|
|
35
|
+
* @returns {{ schema: string, project: string, units: object, nets: object[] }}
|
|
36
36
|
*/
|
|
37
37
|
static buildNetlistJson(bundle) {
|
|
38
38
|
const projectName =
|
|
@@ -61,6 +61,10 @@ export class ProjectNetlistExporter {
|
|
|
61
61
|
return {
|
|
62
62
|
schema: 'altium-toolkit.netlist.a1',
|
|
63
63
|
project: projectName,
|
|
64
|
+
units: {
|
|
65
|
+
coordinate: 'mil',
|
|
66
|
+
length: 'mil'
|
|
67
|
+
},
|
|
64
68
|
nets
|
|
65
69
|
}
|
|
66
70
|
}
|