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.
Files changed (88) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/styles/altium-renderers.css +19 -0
  77. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  78. package/src/ui/PcbInteractionIndex.mjs +9 -4
  79. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  80. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  81. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  82. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  83. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  84. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  85. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  86. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  87. package/src/ui/SchematicTypography.mjs +48 -5
  88. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,477 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
6
+ import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
7
+
8
+ /**
9
+ * Composes parsed project, schematic, and PCB models into one read-only design
10
+ * bundle for multi-document consumers.
11
+ */
12
+ export class ProjectDesignBundleBuilder {
13
+ /**
14
+ * Builds a normalized project/design bundle from already parsed models.
15
+ * @param {{ projectModel?: object, documentModels?: object[], annotationModels?: object[], variantName?: string }} options Bundle options.
16
+ * @returns {object}
17
+ */
18
+ static build(options = {}) {
19
+ const projectModel = options.projectModel || {}
20
+ const documentModels = Array.isArray(options.documentModels)
21
+ ? options.documentModels
22
+ : []
23
+ const project = projectModel.project || {}
24
+ const documents = project.documents || []
25
+ const schematicModels = documentModels.filter(
26
+ (model) => model?.kind === 'schematic'
27
+ )
28
+ const pcbModels = documentModels.filter(
29
+ (model) => model?.kind === 'pcb'
30
+ )
31
+ const sheets = ProjectDesignBundleBuilder.#buildSheets(
32
+ schematicModels,
33
+ documents
34
+ )
35
+ const components = ProjectDesignBundleBuilder.#buildComponents(
36
+ schematicModels,
37
+ pcbModels
38
+ )
39
+ const pnp = ProjectDesignBundleBuilder.#buildPnp(pcbModels)
40
+ const nets = ProjectDesignBundleBuilder.#buildNets(
41
+ schematicModels,
42
+ pcbModels
43
+ )
44
+ const bom = ProjectDesignBundleBuilder.#buildBom(documentModels)
45
+ const annotations = ProjectDesignBundleBuilder.#buildAnnotations(
46
+ options.annotationModels || []
47
+ )
48
+ const indexes = ProjectDesignBundleBuilder.#buildIndexes(
49
+ documents,
50
+ sheets,
51
+ components,
52
+ nets,
53
+ pnp
54
+ )
55
+ const bundle = NormalizedModelSchema.attach({
56
+ kind: 'design-bundle',
57
+ fileType: 'ProjectDesignBundle',
58
+ fileName: projectModel.fileName || 'design-bundle.json',
59
+ summary: {
60
+ title:
61
+ projectModel.summary?.title ||
62
+ project.name ||
63
+ 'Project design bundle',
64
+ sheetCount: sheets.length,
65
+ componentCount: components.length,
66
+ netCount: nets.length,
67
+ pnpCount: pnp.entries.length,
68
+ variantCount: (project.variants || []).length,
69
+ annotationMappingCount: annotations.mappings.length
70
+ },
71
+ diagnostics: [
72
+ {
73
+ severity: 'info',
74
+ message:
75
+ 'Composed ' +
76
+ documentModels.length +
77
+ ' parsed document models into a project design bundle.'
78
+ }
79
+ ],
80
+ project,
81
+ variants: project.variants || [],
82
+ sheets,
83
+ components,
84
+ schematic_hierarchy:
85
+ ProjectDesignBundleBuilder.#buildSchematicHierarchy(
86
+ project,
87
+ schematicModels,
88
+ documents
89
+ ),
90
+ pnp,
91
+ nets,
92
+ annotations,
93
+ indexes,
94
+ bom
95
+ })
96
+
97
+ if (options.variantName) {
98
+ bundle.effectiveVariant = ProjectVariantViewBuilder.build(bundle, {
99
+ variantName: options.variantName
100
+ })
101
+ }
102
+
103
+ return bundle
104
+ }
105
+
106
+ /**
107
+ * Builds sheet entries from schematic models and project document rows.
108
+ * @param {object[]} schematicModels Parsed schematic models.
109
+ * @param {object[]} documents Project document rows.
110
+ * @returns {object[]}
111
+ */
112
+ static #buildSheets(schematicModels, documents) {
113
+ return schematicModels.map((model, index) => {
114
+ const document = ProjectDesignBundleBuilder.#documentForModel(
115
+ model,
116
+ documents
117
+ )
118
+
119
+ return {
120
+ bundleIndex: index,
121
+ fileName: model.fileName,
122
+ title: model.summary?.title || model.fileName,
123
+ documentPath: document?.path || model.fileName,
124
+ uniqueId: document?.uniqueId || '',
125
+ sheet: model.schematic?.sheet || {},
126
+ componentCount: model.schematic?.components?.length || 0,
127
+ netCount: model.schematic?.nets?.length || 0
128
+ }
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Builds component entries joined by designator across schematic and PCB
134
+ * documents.
135
+ * @param {object[]} schematicModels Parsed schematic models.
136
+ * @param {object[]} pcbModels Parsed PCB models.
137
+ * @returns {object[]}
138
+ */
139
+ static #buildComponents(schematicModels, pcbModels) {
140
+ const componentsByDesignator = new Map()
141
+
142
+ for (const model of schematicModels) {
143
+ for (const component of model.schematic?.components || []) {
144
+ const entry = ProjectDesignBundleBuilder.#componentEntry(
145
+ componentsByDesignator,
146
+ component.designator
147
+ )
148
+ entry.schematic = {
149
+ fileName: model.fileName,
150
+ uniqueId: component.uniqueId || '',
151
+ libReference: component.libReference || '',
152
+ value: component.value || ''
153
+ }
154
+ }
155
+ }
156
+
157
+ for (const model of pcbModels) {
158
+ for (const component of model.pcb?.components || []) {
159
+ const entry = ProjectDesignBundleBuilder.#componentEntry(
160
+ componentsByDesignator,
161
+ component.designator
162
+ )
163
+ entry.pcb = {
164
+ fileName: model.fileName,
165
+ componentIndex: component.componentIndex,
166
+ uniqueId: component.uniqueId || '',
167
+ pattern: component.pattern || ''
168
+ }
169
+ }
170
+ }
171
+
172
+ return [...componentsByDesignator.values()].map((component, index) => ({
173
+ bundleIndex: index,
174
+ ...component
175
+ }))
176
+ }
177
+
178
+ /**
179
+ * Gets or creates one component bundle entry.
180
+ * @param {Map<string, object>} componentsByDesignator Component map.
181
+ * @param {string} designator Component designator.
182
+ * @returns {object}
183
+ */
184
+ static #componentEntry(componentsByDesignator, designator) {
185
+ const key = String(designator || '').trim()
186
+ if (!componentsByDesignator.has(key)) {
187
+ componentsByDesignator.set(key, {
188
+ designator: key,
189
+ schematic: null,
190
+ pcb: null
191
+ })
192
+ }
193
+ return componentsByDesignator.get(key)
194
+ }
195
+
196
+ /**
197
+ * Builds a combined pick-place model.
198
+ * @param {object[]} pcbModels Parsed PCB models.
199
+ * @returns {object}
200
+ */
201
+ static #buildPnp(pcbModels) {
202
+ const entries = []
203
+ let positionMode = ''
204
+
205
+ for (const model of pcbModels) {
206
+ const pnp = model.pnp || model.pcb?.pickPlace || {}
207
+ positionMode ||= pnp.positionMode || ''
208
+ for (const entry of pnp.entries || []) {
209
+ entries.push({
210
+ bundleIndex: entries.length,
211
+ sourceFileName: model.fileName,
212
+ ...entry
213
+ })
214
+ }
215
+ }
216
+
217
+ return {
218
+ positionMode,
219
+ entries,
220
+ modes: {}
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Builds combined schematic and PCB net entries.
226
+ * @param {object[]} schematicModels Parsed schematic models.
227
+ * @param {object[]} pcbModels Parsed PCB models.
228
+ * @returns {object[]}
229
+ */
230
+ static #buildNets(schematicModels, pcbModels) {
231
+ const netsByName = new Map()
232
+
233
+ for (const model of schematicModels) {
234
+ for (const net of model.schematic?.nets || []) {
235
+ const entry = ProjectDesignBundleBuilder.#netEntry(
236
+ netsByName,
237
+ net.name
238
+ )
239
+ entry.schematic.push({
240
+ fileName: model.fileName,
241
+ pins: net.pins || [],
242
+ labels: net.labels || [],
243
+ segments: net.segments || [],
244
+ ...(model.schematic?.harnesses
245
+ ? { harnesses: model.schematic.harnesses.connectors }
246
+ : {})
247
+ })
248
+ entry.pins.push(...(net.pins || []))
249
+ }
250
+ }
251
+
252
+ for (const model of pcbModels) {
253
+ for (const net of model.pcb?.nets || []) {
254
+ const entry = ProjectDesignBundleBuilder.#netEntry(
255
+ netsByName,
256
+ net.name
257
+ )
258
+ entry.pcb.push({
259
+ fileName: model.fileName,
260
+ netIndex: net.netIndex,
261
+ uniqueId: net.uniqueId || ''
262
+ })
263
+ }
264
+ }
265
+
266
+ return [...netsByName.values()].map((net, index) => ({
267
+ bundleIndex: index,
268
+ ...net
269
+ }))
270
+ }
271
+
272
+ /**
273
+ * Gets or creates one net bundle entry.
274
+ * @param {Map<string, object>} netsByName Net map.
275
+ * @param {string} name Net name.
276
+ * @returns {object}
277
+ */
278
+ static #netEntry(netsByName, name) {
279
+ const key = String(name || '').trim()
280
+ if (!netsByName.has(key)) {
281
+ netsByName.set(key, {
282
+ name: key,
283
+ schematic: [],
284
+ pcb: [],
285
+ pins: []
286
+ })
287
+ }
288
+ return netsByName.get(key)
289
+ }
290
+
291
+ /**
292
+ * Selects a combined BOM, preferring PCB BOM rows when available.
293
+ * @param {object[]} documentModels Parsed document models.
294
+ * @returns {object[]}
295
+ */
296
+ static #buildBom(documentModels) {
297
+ const pcbBom = documentModels
298
+ .filter((model) => model?.kind === 'pcb')
299
+ .flatMap((model) => model.bom || [])
300
+
301
+ if (pcbBom.length) {
302
+ return pcbBom
303
+ }
304
+
305
+ return documentModels.flatMap((model) => model.bom || [])
306
+ }
307
+
308
+ /**
309
+ * Builds compiled-designator annotation mappings.
310
+ * @param {object[]} annotationModels Parsed annotation models.
311
+ * @returns {{ mappings: object[], bySourceDesignator: Record<string, object>, byCompiledDesignator: Record<string, object> }}
312
+ */
313
+ static #buildAnnotations(annotationModels) {
314
+ const mappings = []
315
+
316
+ for (const model of annotationModels || []) {
317
+ mappings.push(...(model?.annotations?.mappings || []))
318
+ }
319
+
320
+ return {
321
+ mappings,
322
+ bySourceDesignator: ProjectDesignBundleBuilder.#indexFullBy(
323
+ mappings,
324
+ 'sourceDesignator'
325
+ ),
326
+ byCompiledDesignator: ProjectDesignBundleBuilder.#indexFullBy(
327
+ mappings,
328
+ 'compiledDesignator'
329
+ )
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Builds schematic hierarchy metadata.
335
+ * @param {object} project Project model.
336
+ * @param {object[]} schematicModels Parsed schematic models.
337
+ * @param {object[]} documents Project document rows.
338
+ * @returns {object}
339
+ */
340
+ static #buildSchematicHierarchy(project, schematicModels, documents) {
341
+ const hierarchy = {
342
+ mode: project?.design?.HierarchyMode || '',
343
+ modeName: ProjectDesignBundleBuilder.#hierarchyModeName(
344
+ project?.design?.HierarchyMode
345
+ ),
346
+ sheets: schematicModels.map((model) => {
347
+ const document = ProjectDesignBundleBuilder.#documentForModel(
348
+ model,
349
+ documents
350
+ )
351
+ return {
352
+ fileName: model.fileName,
353
+ documentPath: document?.path || model.fileName,
354
+ uniqueId: document?.uniqueId || '',
355
+ title: model.summary?.title || model.fileName
356
+ }
357
+ }),
358
+ sheetSymbols: schematicModels.flatMap((model) =>
359
+ (model.schematic?.sheetSymbols || []).map((sheetSymbol) => ({
360
+ sheetFileName: model.fileName,
361
+ uniqueId: sheetSymbol.uniqueId || '',
362
+ entries: (model.schematic?.sheetEntries || []).map(
363
+ (entry) => entry.name
364
+ )
365
+ }))
366
+ )
367
+ }
368
+ const harnessBundleLinks = schematicModels.flatMap((model) =>
369
+ (model.schematic?.harnesses?.bundleLinks || []).map((link) => ({
370
+ sheetFileName: model.fileName,
371
+ ...link
372
+ }))
373
+ )
374
+
375
+ if (harnessBundleLinks.length) {
376
+ hierarchy.harness_bundle_links = harnessBundleLinks
377
+ }
378
+
379
+ return hierarchy
380
+ }
381
+
382
+ /**
383
+ * Builds bundle lookup indexes.
384
+ * @param {object[]} documents Project document rows.
385
+ * @param {object[]} sheets Bundle sheets.
386
+ * @param {object[]} components Bundle components.
387
+ * @param {object[]} nets Bundle nets.
388
+ * @param {object} pnp Bundle PnP model.
389
+ * @returns {object}
390
+ */
391
+ static #buildIndexes(documents, sheets, components, nets, pnp) {
392
+ return {
393
+ documentsByPath: ProjectDesignBundleBuilder.#indexBy(
394
+ documents,
395
+ 'normalizedPath'
396
+ ),
397
+ sheetsByFileName: ProjectDesignBundleBuilder.#indexBy(
398
+ sheets,
399
+ 'fileName'
400
+ ),
401
+ componentsByDesignator: ProjectDesignBundleBuilder.#indexBy(
402
+ components,
403
+ 'designator'
404
+ ),
405
+ netsByName: ProjectDesignBundleBuilder.#indexBy(nets, 'name'),
406
+ pnpByDesignator: ProjectDesignBundleBuilder.#indexBy(
407
+ pnp.entries,
408
+ 'designator'
409
+ )
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Builds a compact object index by a field.
415
+ * @param {object[]} records Records to index.
416
+ * @param {string} key Field name.
417
+ * @returns {Record<string, object>}
418
+ */
419
+ static #indexBy(records, key) {
420
+ const index = {}
421
+ for (const record of records || []) {
422
+ const value = String(record?.[key] || '').trim()
423
+ if (!value) continue
424
+ index[value] = {
425
+ bundleIndex: record.bundleIndex ?? record.index ?? 0
426
+ }
427
+ }
428
+ return index
429
+ }
430
+
431
+ /**
432
+ * Builds a full object index by a field.
433
+ * @param {object[]} records Records to index.
434
+ * @param {string} key Field name.
435
+ * @returns {Record<string, object>}
436
+ */
437
+ static #indexFullBy(records, key) {
438
+ const index = {}
439
+ for (const record of records || []) {
440
+ const value = String(record?.[key] || '').trim()
441
+ if (value) index[value] = record
442
+ }
443
+ return index
444
+ }
445
+
446
+ /**
447
+ * Finds the project document row corresponding to a parsed model.
448
+ * @param {object} model Parsed document model.
449
+ * @param {object[]} documents Project document rows.
450
+ * @returns {object | null}
451
+ */
452
+ static #documentForModel(model, documents) {
453
+ return (
454
+ (documents || []).find(
455
+ (document) => document.fileName === model.fileName
456
+ ) || null
457
+ )
458
+ }
459
+
460
+ /**
461
+ * Resolves a display name for a project hierarchy mode.
462
+ * @param {string | number | undefined} mode Raw hierarchy mode.
463
+ * @returns {string}
464
+ */
465
+ static #hierarchyModeName(mode) {
466
+ switch (String(mode || '')) {
467
+ case '2':
468
+ return 'hierarchical'
469
+ case '1':
470
+ return 'flat'
471
+ case '3':
472
+ return 'global'
473
+ default:
474
+ return 'unspecified'
475
+ }
476
+ }
477
+ }