altium-toolkit 1.0.8 → 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 (102) 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/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -13,6 +13,9 @@ import { SchematicPinParser } from './SchematicPinParser.mjs'
13
13
  import { SchematicPrimitiveParser } from './SchematicPrimitiveParser.mjs'
14
14
  import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
15
15
  import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
16
+ import { IntLibModelParser } from './IntLibModelParser.mjs'
17
+ import { IntLibStreamExtractor } from './IntLibStreamExtractor.mjs'
18
+ import { DraftsmanDigestParser } from './DraftsmanDigestParser.mjs'
16
19
  import { PcbModelParser } from './PcbModelParser.mjs'
17
20
  import { PcbLibModelParser } from './PcbLibModelParser.mjs'
18
21
  import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
@@ -25,8 +28,20 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
25
28
  import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
26
29
  import { SchematicImageParser } from './SchematicImageParser.mjs'
27
30
  import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
31
+ import { SchematicRecordTypeRegistry } from './SchematicRecordTypeRegistry.mjs'
28
32
  import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
33
+ import { SchematicComponentOwnerTextResolver } from './SchematicComponentOwnerTextResolver.mjs'
34
+ import { SchematicOwnershipGraphParser } from './SchematicOwnershipGraphParser.mjs'
29
35
  import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
36
+ import { SchematicTemplateParser } from './SchematicTemplateParser.mjs'
37
+ import { SchematicHarnessParser } from './SchematicHarnessParser.mjs'
38
+ import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
39
+ import { SchematicCrossSheetConnectorParser } from './SchematicCrossSheetConnectorParser.mjs'
40
+ import { SchematicRepeatedChannelParser } from './SchematicRepeatedChannelParser.mjs'
41
+ import { SchematicDisplayModeCatalogParser } from './SchematicDisplayModeCatalogParser.mjs'
42
+ import { SchematicBindingProvenanceParser } from './SchematicBindingProvenanceParser.mjs'
43
+ import { SchematicConnectivityQaBuilder } from './SchematicConnectivityQaBuilder.mjs'
44
+ import { SchematicQaReportBuilder } from './SchematicQaReportBuilder.mjs'
30
45
  import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
31
46
  import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
32
47
  const {
@@ -41,6 +56,7 @@ const {
41
56
  } = ParserUtils
42
57
  const {
43
58
  extractSchematicFonts,
59
+ extractSchematicFontDiagnostics,
44
60
  extractSchematicMetadata,
45
61
  extractSchematicTitleBlock,
46
62
  normalizeSchematicTextRecord
@@ -74,7 +90,7 @@ export class AltiumParser {
74
90
  * Parses a native Altium buffer into the renderer compatibility model.
75
91
  * @param {string} fileName
76
92
  * @param {ArrayBuffer} arrayBuffer
77
- * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
93
+ * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project' | 'integrated-library' | 'draftsman', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib' | 'PCBDwf', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], schematic?: Record<string, unknown>, pcb?: Record<string, unknown>, pcbLibrary?: Record<string, unknown>, project?: Record<string, unknown>, integratedLibrary?: Record<string, unknown>, draftsman?: Record<string, unknown>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
78
94
  */
79
95
  static parseArrayBufferToRendererModel(fileName, arrayBuffer) {
80
96
  const records = AsciiRecordParser.parse(arrayBuffer)
@@ -85,7 +101,8 @@ export class AltiumParser {
85
101
  return AltiumParser.#parseSchematic(
86
102
  fileName,
87
103
  schematicExtraction?.records || records,
88
- arrayBuffer
104
+ arrayBuffer,
105
+ schematicExtraction
89
106
  )
90
107
  }
91
108
  if (fileType === 'PcbDoc') {
@@ -106,6 +123,15 @@ export class AltiumParser {
106
123
  if (fileType === 'PrjPcb') {
107
124
  return PrjPcbModelParser.parse(fileName, arrayBuffer)
108
125
  }
126
+ if (fileType === 'IntLib') {
127
+ return IntLibModelParser.parse(
128
+ fileName,
129
+ IntLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
130
+ )
131
+ }
132
+ if (fileType === 'PCBDwf') {
133
+ return DraftsmanDigestParser.parse(fileName, arrayBuffer)
134
+ }
109
135
  throw new Error('Unsupported file type: ' + fileName)
110
136
  }
111
137
 
@@ -113,7 +139,7 @@ export class AltiumParser {
113
139
  * Chooses the format based on extension and content.
114
140
  * @param {string} fileName
115
141
  * @param {{ fields: Record<string, string | string[]> }[]} records
116
- * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
142
+ * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib' | 'PCBDwf'}
117
143
  */
118
144
  static #sniffFileType(fileName, records) {
119
145
  const normalized = String(fileName || '').toLowerCase()
@@ -121,6 +147,8 @@ export class AltiumParser {
121
147
  if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
122
148
  if (normalized.endsWith('.pcblib')) return 'PcbLib'
123
149
  if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
150
+ if (normalized.endsWith('.intlib')) return 'IntLib'
151
+ if (normalized.endsWith('.pcbdwf')) return 'PCBDwf'
124
152
 
125
153
  const hasSchematicHeader = records.some((record) =>
126
154
  getField(record.fields, 'HEADER').includes('Schematic')
@@ -132,12 +160,23 @@ export class AltiumParser {
132
160
  * @param {string} fileName
133
161
  * @param {{ raw: string, fields: Record<string, string | string[]> }[]} records
134
162
  * @param {ArrayBuffer} arrayBuffer
163
+ * @param {{ embeddedFiles?: object } | null} schematicExtraction
135
164
  * @returns {ReturnType<typeof AltiumParser.parseArrayBufferToRendererModel>}
136
165
  */
137
- static #parseSchematic(fileName, records, arrayBuffer) {
138
- const componentRecords = records.filter(
166
+ static #parseSchematic(
167
+ fileName,
168
+ records,
169
+ arrayBuffer,
170
+ schematicExtraction = null
171
+ ) {
172
+ const recordIndexAwareRecords = records.map((record, recordIndex) => ({
173
+ ...record,
174
+ recordIndex
175
+ }))
176
+ const componentRecords = recordIndexAwareRecords.filter(
139
177
  (record) => getField(record.fields, 'RECORD') === '1'
140
178
  )
179
+ const recordTypes = SchematicRecordTypeRegistry.summarize(records)
141
180
  const ownersWithImplicitDisplayMode =
142
181
  AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
143
182
  const activeMultipartOwnerParts =
@@ -176,6 +215,10 @@ export class AltiumParser {
176
215
  getField(record.fields, 'RECORD') !== '30' &&
177
216
  getField(record.fields, 'RECORD') !== '37' &&
178
217
  !SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
218
+ !SchematicPrimitiveParser.isRoundedRectangleRecord(
219
+ record.fields
220
+ ) &&
221
+ !SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
179
222
  !AltiumParser.#hasDisplayText(record.fields) &&
180
223
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
181
224
  AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
@@ -192,12 +235,34 @@ export class AltiumParser {
192
235
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
193
236
  AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
194
237
  )
238
+ const roundedRectangleRecords = drawableRecords.filter(
239
+ (record) =>
240
+ SchematicPrimitiveParser.isRoundedRectangleRecord(
241
+ record.fields
242
+ ) &&
243
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
244
+ AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
245
+ )
246
+ const ieeeSymbolRecords = drawableRecords.filter(
247
+ (record) =>
248
+ SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
249
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location')
250
+ )
195
251
  const arcRecords = drawableRecords.filter(
196
252
  (record) =>
197
253
  ['11', '12'].includes(getField(record.fields, 'RECORD')) &&
198
254
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
199
255
  parseNumericField(record.fields, 'Radius') !== null
200
256
  )
257
+ const bezierRecords = drawableRecords.filter(
258
+ (record) => getField(record.fields, 'RECORD') === '5'
259
+ )
260
+ const pieRecords = drawableRecords.filter(
261
+ (record) =>
262
+ getField(record.fields, 'RECORD') === '9' &&
263
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
264
+ parseNumericField(record.fields, 'Radius') !== null
265
+ )
201
266
  const ellipseRecords = drawableRecords.filter(
202
267
  (record) =>
203
268
  getField(record.fields, 'RECORD') === '8' &&
@@ -227,10 +292,26 @@ export class AltiumParser {
227
292
  const crossRecords = drawableRecords.filter(
228
293
  (record) => getField(record.fields, 'RECORD') === '22'
229
294
  )
230
- const recordIndexAwareRecords = records.map((record, recordIndex) => ({
231
- ...record,
232
- recordIndex
233
- }))
295
+ const ownership = SchematicOwnershipGraphParser.parse(
296
+ recordIndexAwareRecords
297
+ )
298
+ const harnesses = SchematicHarnessParser.parse(recordIndexAwareRecords)
299
+ const implementations = SchematicImplementationParser.parse(
300
+ recordIndexAwareRecords
301
+ )
302
+ const displayModes = SchematicDisplayModeCatalogParser.parse(
303
+ recordIndexAwareRecords
304
+ )
305
+ const bindings = SchematicBindingProvenanceParser.parse(
306
+ recordIndexAwareRecords,
307
+ implementations
308
+ )
309
+ const crossSheetConnectors = SchematicCrossSheetConnectorParser.parse(
310
+ recordIndexAwareRecords
311
+ )
312
+ const repeatedChannels = SchematicRepeatedChannelParser.parse(
313
+ recordIndexAwareRecords
314
+ )
234
315
  const relatedTexts = new Map()
235
316
 
236
317
  for (const record of records) {
@@ -244,6 +325,9 @@ export class AltiumParser {
244
325
 
245
326
  const metadataTexts = extractSchematicMetadata(textRecords)
246
327
  const schematicFonts = extractSchematicFonts(sheetRecord?.fields)
328
+ const schematicRenderDiagnostics = extractSchematicFontDiagnostics(
329
+ sheetRecord?.fields
330
+ )
247
331
  const sheetWidth =
248
332
  parseNumericField(sheetRecord?.fields, 'CustomX') || 1500
249
333
  const sheetHeight =
@@ -343,6 +427,9 @@ export class AltiumParser {
343
427
  polygons
344
428
  ))
345
429
  const arcs = SchematicPrimitiveParser.parseSchematicArcs(arcRecords)
430
+ const beziers =
431
+ SchematicPrimitiveParser.parseSchematicBeziers(bezierRecords)
432
+ const pies = SchematicPrimitiveParser.parseSchematicPies(pieRecords)
346
433
  const ellipses =
347
434
  SchematicPrimitiveParser.parseSchematicEllipses(ellipseRecords)
348
435
  const rectangles =
@@ -358,8 +445,20 @@ export class AltiumParser {
358
445
  )
359
446
  const regions =
360
447
  SchematicPrimitiveParser.parseSchematicRegions(regionRecords)
448
+ const roundedRectangles =
449
+ SchematicPrimitiveParser.parseSchematicRoundedRectangles(
450
+ roundedRectangleRecords
451
+ )
452
+ const ieeeSymbols =
453
+ SchematicPrimitiveParser.parseSchematicIeeeSymbols(
454
+ ieeeSymbolRecords
455
+ )
361
456
  const directives =
362
457
  SchematicDirectiveParser.parseSchematicDirectives(directiveRecords)
458
+ const directiveSemantics =
459
+ SchematicDirectiveParser.parseDirectiveSemantics(
460
+ recordIndexAwareRecords
461
+ )
363
462
  const { sheetSymbols, sheetEntries } = SchematicSheetParser.parse(
364
463
  recordIndexAwareRecords
365
464
  )
@@ -371,6 +470,11 @@ export class AltiumParser {
371
470
  recordIndexAwareRecords,
372
471
  arrayBuffer
373
472
  )
473
+ const template = SchematicTemplateParser.parse(
474
+ recordIndexAwareRecords,
475
+ sheetRecord,
476
+ sheet
477
+ )
374
478
 
375
479
  const ports = parseSchematicPorts(portRecords, lines)
376
480
  const crosses = parseSchematicCrosses(crossRecords)
@@ -421,6 +525,8 @@ export class AltiumParser {
421
525
  pins,
422
526
  ports
423
527
  )
528
+ const textFrames =
529
+ SchematicTextParser.extractSchematicTextFrames(anchoredTexts)
424
530
 
425
531
  const components = componentRecords.map((record) => {
426
532
  const x = parseNumericField(record.fields, 'Location.X') || 0
@@ -428,25 +534,28 @@ export class AltiumParser {
428
534
  const libReference =
429
535
  getField(record.fields, 'LibReference') ||
430
536
  getField(record.fields, 'DesignItemId')
431
- const ownerIndex = String(
432
- (parseNumericField(record.fields, 'IndexInSheet') || 0) + 1
537
+ const ownerTexts =
538
+ SchematicComponentOwnerTextResolver.resolveOwnerTexts(
539
+ record,
540
+ recordIndexAwareRecords,
541
+ relatedTexts
542
+ )
543
+
544
+ const designator = SchematicComponentTextResolver.resolveDesignator(
545
+ ownerTexts,
546
+ anchoredTexts,
547
+ {
548
+ x,
549
+ y,
550
+ libReference
551
+ }
433
552
  )
434
- const ownerTexts = relatedTexts.get(ownerIndex) || []
435
553
 
436
554
  return {
437
555
  x,
438
556
  y,
439
557
  libReference,
440
- designator:
441
- SchematicComponentTextResolver.resolveDesignator(
442
- ownerTexts,
443
- anchoredTexts,
444
- {
445
- x,
446
- y,
447
- libReference
448
- }
449
- ) || 'U?',
558
+ designator: designator === null ? 'U?' : designator,
450
559
  value: SchematicComponentTextResolver.resolveValue(
451
560
  ownerTexts,
452
561
  anchoredTexts,
@@ -513,6 +622,16 @@ export class AltiumParser {
513
622
  'Sheet metadata record 31 was not found. Using fallback dimensions.'
514
623
  })
515
624
  }
625
+ diagnostics.push(
626
+ ...schematicRenderDiagnostics.fontFallbacks.map((fallback) => ({
627
+ severity: fallback.severity,
628
+ code: fallback.code,
629
+ fontId: fallback.fontId,
630
+ sourceFamily: fallback.sourceFamily,
631
+ resolvedFamily: fallback.resolvedFamily,
632
+ message: fallback.message
633
+ }))
634
+ )
516
635
  diagnostics.push(...imageDiagnostics)
517
636
  const { nets, diagnostics: netDiagnostics } =
518
637
  SchematicNetlistBuilder.build({
@@ -520,11 +639,36 @@ export class AltiumParser {
520
639
  texts: anchoredTexts,
521
640
  pins,
522
641
  ports,
642
+ crossSheetConnectors: crossSheetConnectors?.connectors || [],
523
643
  junctions,
524
644
  busEntries,
525
645
  sheetEntries
526
646
  })
527
647
  diagnostics.push(...netDiagnostics)
648
+ const qa = SchematicQaReportBuilder.build({
649
+ records: recordIndexAwareRecords,
650
+ sheet: resolvedSheet,
651
+ lines: normalizedLines,
652
+ texts: anchoredTexts
653
+ })
654
+ const connectivityQa = SchematicConnectivityQaBuilder.build({
655
+ nets,
656
+ texts: anchoredTexts,
657
+ pins,
658
+ ports,
659
+ junctions
660
+ })
661
+ const embeddedFiles = schematicExtraction?.embeddedFiles || null
662
+
663
+ if (embeddedFiles?.diagnostics?.length) {
664
+ diagnostics.push(
665
+ ...embeddedFiles.diagnostics.map((issue) => ({
666
+ severity: issue.severity === 'info' ? 'info' : 'warning',
667
+ code: issue.code,
668
+ message: issue.message
669
+ }))
670
+ )
671
+ }
528
672
 
529
673
  return NormalizedModelSchema.attach({
530
674
  kind: 'schematic',
@@ -535,19 +679,30 @@ export class AltiumParser {
535
679
  componentCount: components.length,
536
680
  lineCount: lines.length,
537
681
  textCount: anchoredTexts.length,
682
+ recordTypeCount: recordTypes.length,
538
683
  bomRowCount: bom.length
539
684
  },
540
685
  diagnostics,
541
686
  schematic: {
542
687
  sheet: resolvedSheet,
688
+ recordTypes,
689
+ ...(schematicRenderDiagnostics.fontFallbacks.length
690
+ ? { renderDiagnostics: schematicRenderDiagnostics }
691
+ : {}),
543
692
  lines: normalizedLines,
544
693
  polygons,
545
694
  rectangles,
695
+ roundedRectangles,
546
696
  regions,
547
697
  ellipses,
548
698
  arcs,
699
+ beziers,
700
+ pies,
701
+ ieeeSymbols,
549
702
  directives,
703
+ directiveSemantics,
550
704
  texts: anchoredTexts,
705
+ textFrames,
551
706
  components,
552
707
  pins,
553
708
  ports,
@@ -560,7 +715,22 @@ export class AltiumParser {
560
715
  junctions,
561
716
  busEntries,
562
717
  images,
563
- nets
718
+ nets,
719
+ ownership,
720
+ ...(template ? { template } : {}),
721
+ ...(harnesses ? { harnesses } : {}),
722
+ ...(implementations ? { implementations } : {}),
723
+ ...(displayModes ? { displayModes } : {}),
724
+ ...(bindings ? { bindings } : {}),
725
+ ...(crossSheetConnectors ? { crossSheetConnectors } : {}),
726
+ ...(repeatedChannels ? { repeatedChannels } : {}),
727
+ ...(embeddedFiles &&
728
+ (embeddedFiles.files?.length ||
729
+ embeddedFiles.diagnostics?.length)
730
+ ? { embeddedFiles }
731
+ : {}),
732
+ qa,
733
+ connectivityQa
564
734
  },
565
735
  bom
566
736
  })
@@ -598,7 +768,9 @@ export class AltiumParser {
598
768
  ownerPartId &&
599
769
  ownerPartId !== '-1' &&
600
770
  !getField(record.fields, 'OwnerPartDisplayMode') &&
601
- AltiumParser.#isDisplayModeSelectablePrimitive(record.fields)
771
+ SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
772
+ record.fields
773
+ )
602
774
  ) {
603
775
  owners.add(ownerIndex)
604
776
  }
@@ -652,27 +824,6 @@ export class AltiumParser {
652
824
  )
653
825
  }
654
826
 
655
- /**
656
- * Returns true when a schematic primitive participates in owner display
657
- * mode selection.
658
- * @param {Record<string, string | string[]>} fields
659
- * @returns {boolean}
660
- */
661
- static #isDisplayModeSelectablePrimitive(fields) {
662
- const recordType = getField(fields, 'RECORD')
663
-
664
- return (
665
- recordType === '2' ||
666
- recordType === '6' ||
667
- recordType === '11' ||
668
- recordType === '12' ||
669
- recordType === '13' ||
670
- recordType === '27' ||
671
- (AltiumParser.#hasCoordinatePair(fields, 'Location') &&
672
- AltiumParser.#hasCoordinatePair(fields, 'Corner'))
673
- )
674
- }
675
-
676
827
  /**
677
828
  * Groups designators into BOM rows.
678
829
  * @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
@@ -0,0 +1,202 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbSvgRenderer } from '../../ui/PcbSvgRenderer.mjs'
6
+ import { SchematicSvgRenderer } from '../../ui/SchematicSvgRenderer.mjs'
7
+ import { PcbStatisticsBuilder } from './PcbStatisticsBuilder.mjs'
8
+ import { ProjectDesignBundleBuilder } from './ProjectDesignBundleBuilder.mjs'
9
+ import { ProjectDocumentGraphBuilder } from './ProjectDocumentGraphBuilder.mjs'
10
+ import { ProjectNetlistExporter } from './ProjectNetlistExporter.mjs'
11
+
12
+ /**
13
+ * Builds one deterministic CI artifact package from parsed project documents.
14
+ */
15
+ export class CiArtifactBundleBuilder {
16
+ static SCHEMA = 'altium-toolkit.ci.artifact-bundle.a1'
17
+
18
+ static #UNITS = {
19
+ coordinate: 'mil',
20
+ length: 'mil',
21
+ board: 'mil',
22
+ pnp: 'mil',
23
+ angle: 'deg'
24
+ }
25
+
26
+ static #PNP_UNITS = {
27
+ coordinate: 'mil',
28
+ angle: 'deg'
29
+ }
30
+
31
+ /**
32
+ * Builds a deterministic bundle of normalized, rendered, and report outputs.
33
+ * @param {{ projectModel?: object, documentModels?: object[], designBundle?: object, annotationModels?: object[], variantName?: string, renderSchematicSvg?: boolean, renderPcbLayerSvgs?: boolean, schematicSvgOptions?: object }} options Bundle options.
34
+ * @returns {object}
35
+ */
36
+ static build(options = {}) {
37
+ const documentModels = Array.isArray(options.documentModels)
38
+ ? options.documentModels
39
+ : []
40
+ const designBundle =
41
+ options.designBundle ||
42
+ ProjectDesignBundleBuilder.build({
43
+ projectModel: options.projectModel,
44
+ documentModels,
45
+ annotationModels: options.annotationModels || [],
46
+ variantName: options.variantName
47
+ })
48
+ const activeBundle = designBundle.effectiveVariant || designBundle
49
+ const schematicSvgs =
50
+ options.renderSchematicSvg === false
51
+ ? []
52
+ : CiArtifactBundleBuilder.#schematicSvgs(
53
+ documentModels,
54
+ options.schematicSvgOptions || {}
55
+ )
56
+ const pcbLayerSvgs =
57
+ options.renderPcbLayerSvgs === false
58
+ ? []
59
+ : CiArtifactBundleBuilder.#pcbLayerSvgs(documentModels)
60
+ const statistics = CiArtifactBundleBuilder.#statistics(documentModels)
61
+ const diagnostics = CiArtifactBundleBuilder.#diagnostics(
62
+ designBundle,
63
+ documentModels
64
+ )
65
+ const netlistJson =
66
+ ProjectNetlistExporter.buildNetlistJson(activeBundle)
67
+ const documentGraph =
68
+ designBundle.project?.documentGraph ||
69
+ ProjectDocumentGraphBuilder.build(
70
+ options.projectModel?.project || designBundle.project || {}
71
+ )
72
+
73
+ return {
74
+ schema: CiArtifactBundleBuilder.SCHEMA,
75
+ summary: {
76
+ normalizedModelCount: documentModels.length,
77
+ schematicSvgCount: schematicSvgs.length,
78
+ pcbLayerSvgCount: pcbLayerSvgs.reduce(
79
+ (total, entry) => total + entry.layers.length,
80
+ 0
81
+ ),
82
+ netCount: netlistJson.nets.length,
83
+ bomRowCount: (activeBundle.bom || designBundle.bom || [])
84
+ .length,
85
+ pnpCount: (activeBundle.pnp?.entries || []).length,
86
+ diagnosticCount: diagnostics.length
87
+ },
88
+ units: designBundle.units || CiArtifactBundleBuilder.#UNITS,
89
+ designBundle,
90
+ documentGraph,
91
+ normalizedModels: documentModels,
92
+ netlist: {
93
+ json: netlistJson,
94
+ wirelist: ProjectNetlistExporter.buildWirelist(activeBundle)
95
+ },
96
+ bom: {
97
+ rows: activeBundle.bom || designBundle.bom || []
98
+ },
99
+ pnp: CiArtifactBundleBuilder.#pnp(activeBundle, designBundle),
100
+ schematicSvgs,
101
+ pcbLayerSvgs,
102
+ statistics,
103
+ diagnostics
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Resolves a PnP payload with explicit output units.
109
+ * @param {object} activeBundle Effective bundle or variant.
110
+ * @param {object} designBundle Source design bundle.
111
+ * @returns {object}
112
+ */
113
+ static #pnp(activeBundle, designBundle) {
114
+ const pnp = activeBundle.pnp || designBundle.pnp || { entries: [] }
115
+
116
+ return {
117
+ units: pnp.units || CiArtifactBundleBuilder.#PNP_UNITS,
118
+ ...pnp
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Renders schematic SVG entries.
124
+ * @param {object[]} documentModels Parsed document models.
125
+ * @param {object} renderOptions Schematic SVG render options.
126
+ * @returns {object[]}
127
+ */
128
+ static #schematicSvgs(documentModels, renderOptions) {
129
+ return documentModels
130
+ .filter((model) => model?.kind === 'schematic')
131
+ .map((model) => ({
132
+ fileName: model.fileName || '',
133
+ svg: SchematicSvgRenderer.render(model, renderOptions)
134
+ }))
135
+ }
136
+
137
+ /**
138
+ * Renders per-layer PCB SVG entries.
139
+ * @param {object[]} documentModels Parsed document models.
140
+ * @returns {object[]}
141
+ */
142
+ static #pcbLayerSvgs(documentModels) {
143
+ return documentModels
144
+ .filter((model) => model?.kind === 'pcb')
145
+ .map((model) => ({
146
+ fileName: model.fileName || '',
147
+ layers: PcbSvgRenderer.renderLayerSvgs(model)
148
+ }))
149
+ }
150
+
151
+ /**
152
+ * Builds statistics package entries.
153
+ * @param {object[]} documentModels Parsed document models.
154
+ * @returns {{ pcb: object[] }}
155
+ */
156
+ static #statistics(documentModels) {
157
+ return {
158
+ pcb: documentModels
159
+ .filter((model) => model?.kind === 'pcb')
160
+ .map((model) => ({
161
+ fileName: model.fileName || '',
162
+ statistics:
163
+ model.pcb?.statistics ||
164
+ PcbStatisticsBuilder.build(model.pcb || {})
165
+ }))
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Collects diagnostics from the bundle and source documents.
171
+ * @param {object} designBundle Composed design bundle.
172
+ * @param {object[]} documentModels Parsed document models.
173
+ * @returns {object[]}
174
+ */
175
+ static #diagnostics(designBundle, documentModels) {
176
+ return [
177
+ ...CiArtifactBundleBuilder.#sourceDiagnostics(
178
+ 'design-bundle',
179
+ designBundle.diagnostics || []
180
+ ),
181
+ ...documentModels.flatMap((model) =>
182
+ CiArtifactBundleBuilder.#sourceDiagnostics(
183
+ model.fileName || model.kind || 'document',
184
+ model.diagnostics || []
185
+ )
186
+ )
187
+ ]
188
+ }
189
+
190
+ /**
191
+ * Adds source labels to diagnostics without changing their codes.
192
+ * @param {string} source Diagnostic source label.
193
+ * @param {object[]} diagnostics Source diagnostics.
194
+ * @returns {object[]}
195
+ */
196
+ static #sourceDiagnostics(source, diagnostics) {
197
+ return (diagnostics || []).map((diagnostic) => ({
198
+ source,
199
+ ...diagnostic
200
+ }))
201
+ }
202
+ }