altium-toolkit 1.0.9 → 1.0.10
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/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +160 -1
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -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/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +7 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbModelParser.mjs +29 -4
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +8 -1
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrjPcbModelParser.mjs +24 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +6 -0
- package/src/ui/PcbSvgRenderer.mjs +65 -0
|
@@ -14,6 +14,15 @@ export class PcbStatisticsBuilder {
|
|
|
14
14
|
static build(pcb) {
|
|
15
15
|
return {
|
|
16
16
|
schema: 'altium-toolkit.pcb.statistics.a1',
|
|
17
|
+
units: {
|
|
18
|
+
coordinate: 'mil',
|
|
19
|
+
length: 'mil',
|
|
20
|
+
board: 'mil',
|
|
21
|
+
drill: 'mil',
|
|
22
|
+
thickness: 'mil',
|
|
23
|
+
copperWeight: 'oz',
|
|
24
|
+
angle: 'deg'
|
|
25
|
+
},
|
|
17
26
|
board: PcbStatisticsBuilder.#boardStats(pcb?.boardOutline || {}),
|
|
18
27
|
drills: PcbStatisticsBuilder.#drillStats(
|
|
19
28
|
pcb?.pads || [],
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
6
|
import { ProjectOutJobDigestBuilder } from './ProjectOutJobDigestBuilder.mjs'
|
|
7
|
+
import { ProjectDocumentGraphBuilder } from './ProjectDocumentGraphBuilder.mjs'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Parses Altium PrjPcb INI-style project files into a normalized project
|
|
@@ -58,6 +59,11 @@ export class PrjPcbModelParser {
|
|
|
58
59
|
documents,
|
|
59
60
|
outputGroups
|
|
60
61
|
})
|
|
62
|
+
const documentGraph = ProjectDocumentGraphBuilder.build({
|
|
63
|
+
documents,
|
|
64
|
+
documentGroups,
|
|
65
|
+
outputGroups
|
|
66
|
+
})
|
|
61
67
|
const summary = PrjPcbModelParser.#buildSummary(
|
|
62
68
|
fileName,
|
|
63
69
|
documents,
|
|
@@ -87,6 +93,7 @@ export class PrjPcbModelParser {
|
|
|
87
93
|
configurations,
|
|
88
94
|
outputGroups,
|
|
89
95
|
outJobDigest,
|
|
96
|
+
documentGraph,
|
|
90
97
|
classGeneration,
|
|
91
98
|
sections: PrjPcbModelParser.#serializeSections(sections)
|
|
92
99
|
},
|
|
@@ -229,6 +236,9 @@ export class PrjPcbModelParser {
|
|
|
229
236
|
integratedLibraries: documents.filter(
|
|
230
237
|
(document) => document.kind === 'integrated-library'
|
|
231
238
|
),
|
|
239
|
+
harnessFiles: documents.filter(
|
|
240
|
+
(document) => document.kind === 'harness'
|
|
241
|
+
),
|
|
232
242
|
outJobs: documents.filter(
|
|
233
243
|
(document) => document.kind === 'output-job'
|
|
234
244
|
),
|
|
@@ -699,7 +709,14 @@ export class PrjPcbModelParser {
|
|
|
699
709
|
PrjPcbModelParser.#stringField(fields, 'OutputType' + index) ||
|
|
700
710
|
''
|
|
701
711
|
if (!type) continue
|
|
702
|
-
|
|
712
|
+
const targetPath =
|
|
713
|
+
PrjPcbModelParser.#stringField(
|
|
714
|
+
fields,
|
|
715
|
+
'OutputTargetPath' + index
|
|
716
|
+
) ||
|
|
717
|
+
PrjPcbModelParser.#stringField(fields, 'OutputPath' + index) ||
|
|
718
|
+
''
|
|
719
|
+
const row = {
|
|
703
720
|
index,
|
|
704
721
|
type,
|
|
705
722
|
name:
|
|
@@ -721,7 +738,9 @@ export class PrjPcbModelParser {
|
|
|
721
738
|
fields,
|
|
722
739
|
'OutputDefault' + index
|
|
723
740
|
)
|
|
724
|
-
}
|
|
741
|
+
}
|
|
742
|
+
if (targetPath) row.targetPath = targetPath
|
|
743
|
+
rows.push(row)
|
|
725
744
|
}
|
|
726
745
|
|
|
727
746
|
return rows
|
|
@@ -986,6 +1005,9 @@ export class PrjPcbModelParser {
|
|
|
986
1005
|
return 'pcb-library'
|
|
987
1006
|
case '.intlib':
|
|
988
1007
|
return 'integrated-library'
|
|
1008
|
+
case '.harness':
|
|
1009
|
+
case '.harnessdoc':
|
|
1010
|
+
return 'harness'
|
|
989
1011
|
case '.outjob':
|
|
990
1012
|
return 'output-job'
|
|
991
1013
|
default:
|
|
@@ -10,6 +10,19 @@ import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
|
|
|
10
10
|
* bundle for multi-document consumers.
|
|
11
11
|
*/
|
|
12
12
|
export class ProjectDesignBundleBuilder {
|
|
13
|
+
static #UNITS = {
|
|
14
|
+
coordinate: 'mil',
|
|
15
|
+
length: 'mil',
|
|
16
|
+
board: 'mil',
|
|
17
|
+
pnp: 'mil',
|
|
18
|
+
angle: 'deg'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static #PNP_UNITS = {
|
|
22
|
+
coordinate: 'mil',
|
|
23
|
+
angle: 'deg'
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* Builds a normalized project/design bundle from already parsed models.
|
|
15
28
|
* @param {{ projectModel?: object, documentModels?: object[], annotationModels?: object[], variantName?: string }} options Bundle options.
|
|
@@ -78,6 +91,7 @@ export class ProjectDesignBundleBuilder {
|
|
|
78
91
|
}
|
|
79
92
|
],
|
|
80
93
|
project,
|
|
94
|
+
units: ProjectDesignBundleBuilder.#UNITS,
|
|
81
95
|
variants: project.variants || [],
|
|
82
96
|
sheets,
|
|
83
97
|
components,
|
|
@@ -215,6 +229,7 @@ export class ProjectDesignBundleBuilder {
|
|
|
215
229
|
}
|
|
216
230
|
|
|
217
231
|
return {
|
|
232
|
+
units: ProjectDesignBundleBuilder.#PNP_UNITS,
|
|
218
233
|
positionMode,
|
|
219
234
|
entries,
|
|
220
235
|
modes: {}
|
|
@@ -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
|
}
|