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.
Files changed (30) hide show
  1. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  2. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  3. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
  4. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +160 -1
  5. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
  8. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  9. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  10. package/package.json +1 -1
  11. package/src/core/altium/AltiumParser.mjs +7 -2
  12. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  13. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  14. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  15. package/src/core/altium/PcbModelParser.mjs +29 -4
  16. package/src/core/altium/PcbPadStackParser.mjs +171 -2
  17. package/src/core/altium/PcbPickPlacePositionResolver.mjs +8 -1
  18. package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
  19. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  20. package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
  21. package/src/core/altium/PrjPcbModelParser.mjs +24 -2
  22. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  23. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  24. package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
  25. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  26. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  27. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  28. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  29. package/src/parser.mjs +6 -0
  30. 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
- rows.push({
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
  }