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
@@ -13,6 +13,8 @@ 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'
16
18
  import { PcbModelParser } from './PcbModelParser.mjs'
17
19
  import { PcbLibModelParser } from './PcbLibModelParser.mjs'
18
20
  import { PcbLibStreamExtractor } from './PcbLibStreamExtractor.mjs'
@@ -25,8 +27,20 @@ import { SchematicJunctionParser } from './SchematicJunctionParser.mjs'
25
27
  import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
26
28
  import { SchematicImageParser } from './SchematicImageParser.mjs'
27
29
  import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
30
+ import { SchematicRecordTypeRegistry } from './SchematicRecordTypeRegistry.mjs'
28
31
  import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
32
+ import { SchematicComponentOwnerTextResolver } from './SchematicComponentOwnerTextResolver.mjs'
33
+ import { SchematicOwnershipGraphParser } from './SchematicOwnershipGraphParser.mjs'
29
34
  import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
35
+ import { SchematicTemplateParser } from './SchematicTemplateParser.mjs'
36
+ import { SchematicHarnessParser } from './SchematicHarnessParser.mjs'
37
+ import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
38
+ import { SchematicCrossSheetConnectorParser } from './SchematicCrossSheetConnectorParser.mjs'
39
+ import { SchematicRepeatedChannelParser } from './SchematicRepeatedChannelParser.mjs'
40
+ import { SchematicDisplayModeCatalogParser } from './SchematicDisplayModeCatalogParser.mjs'
41
+ import { SchematicBindingProvenanceParser } from './SchematicBindingProvenanceParser.mjs'
42
+ import { SchematicConnectivityQaBuilder } from './SchematicConnectivityQaBuilder.mjs'
43
+ import { SchematicQaReportBuilder } from './SchematicQaReportBuilder.mjs'
30
44
  import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
31
45
  import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
32
46
  const {
@@ -41,6 +55,7 @@ const {
41
55
  } = ParserUtils
42
56
  const {
43
57
  extractSchematicFonts,
58
+ extractSchematicFontDiagnostics,
44
59
  extractSchematicMetadata,
45
60
  extractSchematicTitleBlock,
46
61
  normalizeSchematicTextRecord
@@ -74,7 +89,7 @@ export class AltiumParser {
74
89
  * Parses a native Altium buffer into the renderer compatibility model.
75
90
  * @param {string} fileName
76
91
  * @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 }[] }}
92
+ * @returns {{ schema: string, kind: 'schematic' | 'pcb' | 'pcb-library' | 'project' | 'integrated-library', fileType: 'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib', 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>, bom: { designators: string[], quantity: number, pattern: string, source: string, value: string }[] }}
78
93
  */
79
94
  static parseArrayBufferToRendererModel(fileName, arrayBuffer) {
80
95
  const records = AsciiRecordParser.parse(arrayBuffer)
@@ -85,7 +100,8 @@ export class AltiumParser {
85
100
  return AltiumParser.#parseSchematic(
86
101
  fileName,
87
102
  schematicExtraction?.records || records,
88
- arrayBuffer
103
+ arrayBuffer,
104
+ schematicExtraction
89
105
  )
90
106
  }
91
107
  if (fileType === 'PcbDoc') {
@@ -106,6 +122,12 @@ export class AltiumParser {
106
122
  if (fileType === 'PrjPcb') {
107
123
  return PrjPcbModelParser.parse(fileName, arrayBuffer)
108
124
  }
125
+ if (fileType === 'IntLib') {
126
+ return IntLibModelParser.parse(
127
+ fileName,
128
+ IntLibStreamExtractor.extractFromArrayBuffer(arrayBuffer)
129
+ )
130
+ }
109
131
  throw new Error('Unsupported file type: ' + fileName)
110
132
  }
111
133
 
@@ -113,7 +135,7 @@ export class AltiumParser {
113
135
  * Chooses the format based on extension and content.
114
136
  * @param {string} fileName
115
137
  * @param {{ fields: Record<string, string | string[]> }[]} records
116
- * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb'}
138
+ * @returns {'SchDoc' | 'PcbDoc' | 'PcbLib' | 'PrjPcb' | 'IntLib'}
117
139
  */
118
140
  static #sniffFileType(fileName, records) {
119
141
  const normalized = String(fileName || '').toLowerCase()
@@ -121,6 +143,7 @@ export class AltiumParser {
121
143
  if (normalized.endsWith('.pcbdoc')) return 'PcbDoc'
122
144
  if (normalized.endsWith('.pcblib')) return 'PcbLib'
123
145
  if (normalized.endsWith('.prjpcb')) return 'PrjPcb'
146
+ if (normalized.endsWith('.intlib')) return 'IntLib'
124
147
 
125
148
  const hasSchematicHeader = records.some((record) =>
126
149
  getField(record.fields, 'HEADER').includes('Schematic')
@@ -132,12 +155,23 @@ export class AltiumParser {
132
155
  * @param {string} fileName
133
156
  * @param {{ raw: string, fields: Record<string, string | string[]> }[]} records
134
157
  * @param {ArrayBuffer} arrayBuffer
158
+ * @param {{ embeddedFiles?: object } | null} schematicExtraction
135
159
  * @returns {ReturnType<typeof AltiumParser.parseArrayBufferToRendererModel>}
136
160
  */
137
- static #parseSchematic(fileName, records, arrayBuffer) {
138
- const componentRecords = records.filter(
161
+ static #parseSchematic(
162
+ fileName,
163
+ records,
164
+ arrayBuffer,
165
+ schematicExtraction = null
166
+ ) {
167
+ const recordIndexAwareRecords = records.map((record, recordIndex) => ({
168
+ ...record,
169
+ recordIndex
170
+ }))
171
+ const componentRecords = recordIndexAwareRecords.filter(
139
172
  (record) => getField(record.fields, 'RECORD') === '1'
140
173
  )
174
+ const recordTypes = SchematicRecordTypeRegistry.summarize(records)
141
175
  const ownersWithImplicitDisplayMode =
142
176
  AltiumParser.#collectOwnersWithImplicitDisplayMode(records)
143
177
  const activeMultipartOwnerParts =
@@ -176,6 +210,10 @@ export class AltiumParser {
176
210
  getField(record.fields, 'RECORD') !== '30' &&
177
211
  getField(record.fields, 'RECORD') !== '37' &&
178
212
  !SchematicPrimitiveParser.isRectangleRecord(record.fields) &&
213
+ !SchematicPrimitiveParser.isRoundedRectangleRecord(
214
+ record.fields
215
+ ) &&
216
+ !SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
179
217
  !AltiumParser.#hasDisplayText(record.fields) &&
180
218
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
181
219
  AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
@@ -192,12 +230,34 @@ export class AltiumParser {
192
230
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
193
231
  AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
194
232
  )
233
+ const roundedRectangleRecords = drawableRecords.filter(
234
+ (record) =>
235
+ SchematicPrimitiveParser.isRoundedRectangleRecord(
236
+ record.fields
237
+ ) &&
238
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
239
+ AltiumParser.#hasCoordinatePair(record.fields, 'Corner')
240
+ )
241
+ const ieeeSymbolRecords = drawableRecords.filter(
242
+ (record) =>
243
+ SchematicPrimitiveParser.isIeeeSymbolRecord(record.fields) &&
244
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location')
245
+ )
195
246
  const arcRecords = drawableRecords.filter(
196
247
  (record) =>
197
248
  ['11', '12'].includes(getField(record.fields, 'RECORD')) &&
198
249
  AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
199
250
  parseNumericField(record.fields, 'Radius') !== null
200
251
  )
252
+ const bezierRecords = drawableRecords.filter(
253
+ (record) => getField(record.fields, 'RECORD') === '5'
254
+ )
255
+ const pieRecords = drawableRecords.filter(
256
+ (record) =>
257
+ getField(record.fields, 'RECORD') === '9' &&
258
+ AltiumParser.#hasCoordinatePair(record.fields, 'Location') &&
259
+ parseNumericField(record.fields, 'Radius') !== null
260
+ )
201
261
  const ellipseRecords = drawableRecords.filter(
202
262
  (record) =>
203
263
  getField(record.fields, 'RECORD') === '8' &&
@@ -227,10 +287,26 @@ export class AltiumParser {
227
287
  const crossRecords = drawableRecords.filter(
228
288
  (record) => getField(record.fields, 'RECORD') === '22'
229
289
  )
230
- const recordIndexAwareRecords = records.map((record, recordIndex) => ({
231
- ...record,
232
- recordIndex
233
- }))
290
+ const ownership = SchematicOwnershipGraphParser.parse(
291
+ recordIndexAwareRecords
292
+ )
293
+ const harnesses = SchematicHarnessParser.parse(recordIndexAwareRecords)
294
+ const implementations = SchematicImplementationParser.parse(
295
+ recordIndexAwareRecords
296
+ )
297
+ const displayModes = SchematicDisplayModeCatalogParser.parse(
298
+ recordIndexAwareRecords
299
+ )
300
+ const bindings = SchematicBindingProvenanceParser.parse(
301
+ recordIndexAwareRecords,
302
+ implementations
303
+ )
304
+ const crossSheetConnectors = SchematicCrossSheetConnectorParser.parse(
305
+ recordIndexAwareRecords
306
+ )
307
+ const repeatedChannels = SchematicRepeatedChannelParser.parse(
308
+ recordIndexAwareRecords
309
+ )
234
310
  const relatedTexts = new Map()
235
311
 
236
312
  for (const record of records) {
@@ -244,6 +320,9 @@ export class AltiumParser {
244
320
 
245
321
  const metadataTexts = extractSchematicMetadata(textRecords)
246
322
  const schematicFonts = extractSchematicFonts(sheetRecord?.fields)
323
+ const schematicRenderDiagnostics = extractSchematicFontDiagnostics(
324
+ sheetRecord?.fields
325
+ )
247
326
  const sheetWidth =
248
327
  parseNumericField(sheetRecord?.fields, 'CustomX') || 1500
249
328
  const sheetHeight =
@@ -343,6 +422,9 @@ export class AltiumParser {
343
422
  polygons
344
423
  ))
345
424
  const arcs = SchematicPrimitiveParser.parseSchematicArcs(arcRecords)
425
+ const beziers =
426
+ SchematicPrimitiveParser.parseSchematicBeziers(bezierRecords)
427
+ const pies = SchematicPrimitiveParser.parseSchematicPies(pieRecords)
346
428
  const ellipses =
347
429
  SchematicPrimitiveParser.parseSchematicEllipses(ellipseRecords)
348
430
  const rectangles =
@@ -358,8 +440,20 @@ export class AltiumParser {
358
440
  )
359
441
  const regions =
360
442
  SchematicPrimitiveParser.parseSchematicRegions(regionRecords)
443
+ const roundedRectangles =
444
+ SchematicPrimitiveParser.parseSchematicRoundedRectangles(
445
+ roundedRectangleRecords
446
+ )
447
+ const ieeeSymbols =
448
+ SchematicPrimitiveParser.parseSchematicIeeeSymbols(
449
+ ieeeSymbolRecords
450
+ )
361
451
  const directives =
362
452
  SchematicDirectiveParser.parseSchematicDirectives(directiveRecords)
453
+ const directiveSemantics =
454
+ SchematicDirectiveParser.parseDirectiveSemantics(
455
+ recordIndexAwareRecords
456
+ )
363
457
  const { sheetSymbols, sheetEntries } = SchematicSheetParser.parse(
364
458
  recordIndexAwareRecords
365
459
  )
@@ -371,6 +465,11 @@ export class AltiumParser {
371
465
  recordIndexAwareRecords,
372
466
  arrayBuffer
373
467
  )
468
+ const template = SchematicTemplateParser.parse(
469
+ recordIndexAwareRecords,
470
+ sheetRecord,
471
+ sheet
472
+ )
374
473
 
375
474
  const ports = parseSchematicPorts(portRecords, lines)
376
475
  const crosses = parseSchematicCrosses(crossRecords)
@@ -421,6 +520,8 @@ export class AltiumParser {
421
520
  pins,
422
521
  ports
423
522
  )
523
+ const textFrames =
524
+ SchematicTextParser.extractSchematicTextFrames(anchoredTexts)
424
525
 
425
526
  const components = componentRecords.map((record) => {
426
527
  const x = parseNumericField(record.fields, 'Location.X') || 0
@@ -428,25 +529,28 @@ export class AltiumParser {
428
529
  const libReference =
429
530
  getField(record.fields, 'LibReference') ||
430
531
  getField(record.fields, 'DesignItemId')
431
- const ownerIndex = String(
432
- (parseNumericField(record.fields, 'IndexInSheet') || 0) + 1
532
+ const ownerTexts =
533
+ SchematicComponentOwnerTextResolver.resolveOwnerTexts(
534
+ record,
535
+ recordIndexAwareRecords,
536
+ relatedTexts
537
+ )
538
+
539
+ const designator = SchematicComponentTextResolver.resolveDesignator(
540
+ ownerTexts,
541
+ anchoredTexts,
542
+ {
543
+ x,
544
+ y,
545
+ libReference
546
+ }
433
547
  )
434
- const ownerTexts = relatedTexts.get(ownerIndex) || []
435
548
 
436
549
  return {
437
550
  x,
438
551
  y,
439
552
  libReference,
440
- designator:
441
- SchematicComponentTextResolver.resolveDesignator(
442
- ownerTexts,
443
- anchoredTexts,
444
- {
445
- x,
446
- y,
447
- libReference
448
- }
449
- ) || 'U?',
553
+ designator: designator === null ? 'U?' : designator,
450
554
  value: SchematicComponentTextResolver.resolveValue(
451
555
  ownerTexts,
452
556
  anchoredTexts,
@@ -513,6 +617,16 @@ export class AltiumParser {
513
617
  'Sheet metadata record 31 was not found. Using fallback dimensions.'
514
618
  })
515
619
  }
620
+ diagnostics.push(
621
+ ...schematicRenderDiagnostics.fontFallbacks.map((fallback) => ({
622
+ severity: fallback.severity,
623
+ code: fallback.code,
624
+ fontId: fallback.fontId,
625
+ sourceFamily: fallback.sourceFamily,
626
+ resolvedFamily: fallback.resolvedFamily,
627
+ message: fallback.message
628
+ }))
629
+ )
516
630
  diagnostics.push(...imageDiagnostics)
517
631
  const { nets, diagnostics: netDiagnostics } =
518
632
  SchematicNetlistBuilder.build({
@@ -520,11 +634,36 @@ export class AltiumParser {
520
634
  texts: anchoredTexts,
521
635
  pins,
522
636
  ports,
637
+ crossSheetConnectors: crossSheetConnectors?.connectors || [],
523
638
  junctions,
524
639
  busEntries,
525
640
  sheetEntries
526
641
  })
527
642
  diagnostics.push(...netDiagnostics)
643
+ const qa = SchematicQaReportBuilder.build({
644
+ records: recordIndexAwareRecords,
645
+ sheet: resolvedSheet,
646
+ lines: normalizedLines,
647
+ texts: anchoredTexts
648
+ })
649
+ const connectivityQa = SchematicConnectivityQaBuilder.build({
650
+ nets,
651
+ texts: anchoredTexts,
652
+ pins,
653
+ ports,
654
+ junctions
655
+ })
656
+ const embeddedFiles = schematicExtraction?.embeddedFiles || null
657
+
658
+ if (embeddedFiles?.diagnostics?.length) {
659
+ diagnostics.push(
660
+ ...embeddedFiles.diagnostics.map((issue) => ({
661
+ severity: issue.severity === 'info' ? 'info' : 'warning',
662
+ code: issue.code,
663
+ message: issue.message
664
+ }))
665
+ )
666
+ }
528
667
 
529
668
  return NormalizedModelSchema.attach({
530
669
  kind: 'schematic',
@@ -535,19 +674,30 @@ export class AltiumParser {
535
674
  componentCount: components.length,
536
675
  lineCount: lines.length,
537
676
  textCount: anchoredTexts.length,
677
+ recordTypeCount: recordTypes.length,
538
678
  bomRowCount: bom.length
539
679
  },
540
680
  diagnostics,
541
681
  schematic: {
542
682
  sheet: resolvedSheet,
683
+ recordTypes,
684
+ ...(schematicRenderDiagnostics.fontFallbacks.length
685
+ ? { renderDiagnostics: schematicRenderDiagnostics }
686
+ : {}),
543
687
  lines: normalizedLines,
544
688
  polygons,
545
689
  rectangles,
690
+ roundedRectangles,
546
691
  regions,
547
692
  ellipses,
548
693
  arcs,
694
+ beziers,
695
+ pies,
696
+ ieeeSymbols,
549
697
  directives,
698
+ directiveSemantics,
550
699
  texts: anchoredTexts,
700
+ textFrames,
551
701
  components,
552
702
  pins,
553
703
  ports,
@@ -560,7 +710,22 @@ export class AltiumParser {
560
710
  junctions,
561
711
  busEntries,
562
712
  images,
563
- nets
713
+ nets,
714
+ ownership,
715
+ ...(template ? { template } : {}),
716
+ ...(harnesses ? { harnesses } : {}),
717
+ ...(implementations ? { implementations } : {}),
718
+ ...(displayModes ? { displayModes } : {}),
719
+ ...(bindings ? { bindings } : {}),
720
+ ...(crossSheetConnectors ? { crossSheetConnectors } : {}),
721
+ ...(repeatedChannels ? { repeatedChannels } : {}),
722
+ ...(embeddedFiles &&
723
+ (embeddedFiles.files?.length ||
724
+ embeddedFiles.diagnostics?.length)
725
+ ? { embeddedFiles }
726
+ : {}),
727
+ qa,
728
+ connectivityQa
564
729
  },
565
730
  bom
566
731
  })
@@ -598,7 +763,9 @@ export class AltiumParser {
598
763
  ownerPartId &&
599
764
  ownerPartId !== '-1' &&
600
765
  !getField(record.fields, 'OwnerPartDisplayMode') &&
601
- AltiumParser.#isDisplayModeSelectablePrimitive(record.fields)
766
+ SchematicComponentOwnerTextResolver.isDisplayModeSelectablePrimitive(
767
+ record.fields
768
+ )
602
769
  ) {
603
770
  owners.add(ownerIndex)
604
771
  }
@@ -652,27 +819,6 @@ export class AltiumParser {
652
819
  )
653
820
  }
654
821
 
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
822
  /**
677
823
  * Groups designators into BOM rows.
678
824
  * @param {{ designator: string, pattern: string, source: string, value: string }[]} entries
@@ -0,0 +1,255 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds a generic inventory for embedded payload streams.
7
+ */
8
+ export class EmbeddedFileInventoryBuilder {
9
+ static SCHEMA_ID = 'altium-toolkit.embedded-files.a1'
10
+
11
+ /**
12
+ * Builds an embedded-file inventory from a stream map.
13
+ * @param {Map<string, Uint8Array>} streams Compound-document streams.
14
+ * @param {{ skipStreamNames?: Iterable<string> }} [options] Inventory options.
15
+ * @returns {{ schema: string, files: object[], diagnostics: object[] }}
16
+ */
17
+ static buildFromStreams(streams, options = {}) {
18
+ const skipStreamNames = new Set(options.skipStreamNames || [])
19
+ const files = []
20
+ const diagnostics = []
21
+
22
+ for (const [sourceStream, bytes] of streams || []) {
23
+ if (
24
+ skipStreamNames.has(sourceStream) ||
25
+ !EmbeddedFileInventoryBuilder.#isEmbeddedPayloadStream(
26
+ sourceStream
27
+ )
28
+ ) {
29
+ continue
30
+ }
31
+
32
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
33
+ diagnostics.push({
34
+ code: 'embedded-file.empty',
35
+ severity: 'warning',
36
+ sourceStream,
37
+ message: 'Embedded payload stream was empty.'
38
+ })
39
+ continue
40
+ }
41
+
42
+ files.push(
43
+ EmbeddedFileInventoryBuilder.#fileRecord(sourceStream, bytes)
44
+ )
45
+ }
46
+
47
+ return {
48
+ schema: EmbeddedFileInventoryBuilder.SCHEMA_ID,
49
+ files: files.sort((left, right) =>
50
+ left.sourceStream.localeCompare(right.sourceStream)
51
+ ),
52
+ diagnostics: diagnostics.sort((left, right) =>
53
+ left.sourceStream.localeCompare(right.sourceStream)
54
+ )
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Returns true when a stream name represents a payload-style embedded file.
60
+ * @param {string} sourceStream Stream name.
61
+ * @returns {boolean}
62
+ */
63
+ static #isEmbeddedPayloadStream(sourceStream) {
64
+ const normalized = String(sourceStream || '')
65
+ if (/\/(?:Data|Header)$/i.test(normalized)) {
66
+ return false
67
+ }
68
+
69
+ return (
70
+ /^(EmbeddedFiles|Embedded|Attachments|Images?)\//i.test(
71
+ normalized
72
+ ) ||
73
+ /^Models\/\d+$/i.test(normalized) ||
74
+ /\.[A-Za-z0-9]{2,8}$/.test(normalized)
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Builds one file inventory row.
80
+ * @param {string} sourceStream Stream name.
81
+ * @param {Uint8Array} bytes Payload bytes.
82
+ * @returns {object}
83
+ */
84
+ static #fileRecord(sourceStream, bytes) {
85
+ return {
86
+ sourceStream,
87
+ name: EmbeddedFileInventoryBuilder.#basename(sourceStream),
88
+ format: EmbeddedFileInventoryBuilder.#format(sourceStream, bytes),
89
+ byteLength: bytes.byteLength,
90
+ checksum: {
91
+ algorithm: 'fnv1a32',
92
+ value: EmbeddedFileInventoryBuilder.#fnv1a32(bytes)
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Resolves the payload basename from a stream path.
99
+ * @param {string} sourceStream Stream name.
100
+ * @returns {string}
101
+ */
102
+ static #basename(sourceStream) {
103
+ return (
104
+ String(sourceStream || '')
105
+ .split('/')
106
+ .filter(Boolean)
107
+ .pop() || ''
108
+ )
109
+ }
110
+
111
+ /**
112
+ * Classifies payload format from extension, magic bytes, and text probes.
113
+ * @param {string} sourceStream Stream name.
114
+ * @param {Uint8Array} bytes Payload bytes.
115
+ * @returns {string}
116
+ */
117
+ static #format(sourceStream, bytes) {
118
+ const lower =
119
+ EmbeddedFileInventoryBuilder.#basename(sourceStream).toLowerCase()
120
+
121
+ if (
122
+ lower.endsWith('.png') ||
123
+ EmbeddedFileInventoryBuilder.#hasPrefix(
124
+ bytes,
125
+ [0x89, 0x50, 0x4e, 0x47]
126
+ )
127
+ ) {
128
+ return 'png'
129
+ }
130
+ if (
131
+ lower.endsWith('.jpg') ||
132
+ lower.endsWith('.jpeg') ||
133
+ EmbeddedFileInventoryBuilder.#hasPrefix(bytes, [0xff, 0xd8, 0xff])
134
+ ) {
135
+ return 'jpeg'
136
+ }
137
+ if (
138
+ lower.endsWith('.gif') ||
139
+ EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('GIF')
140
+ ) {
141
+ return 'gif'
142
+ }
143
+ if (
144
+ lower.endsWith('.bmp') ||
145
+ EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('BM')
146
+ ) {
147
+ return 'bmp'
148
+ }
149
+ if (
150
+ lower.endsWith('.svg') ||
151
+ EmbeddedFileInventoryBuilder.#trimmedText(bytes).startsWith('<svg')
152
+ ) {
153
+ return 'svg'
154
+ }
155
+ if (
156
+ lower.endsWith('.step') ||
157
+ lower.endsWith('.stp') ||
158
+ EmbeddedFileInventoryBuilder.#trimmedText(bytes).startsWith(
159
+ 'ISO-10303-21'
160
+ )
161
+ ) {
162
+ return 'step'
163
+ }
164
+ if (lower.endsWith('.sldprt') || lower.endsWith('.sldasm')) {
165
+ return 'solidworks'
166
+ }
167
+ if (lower.endsWith('.x_t') || lower.endsWith('.xmt_txt')) {
168
+ return 'parasolid-text'
169
+ }
170
+ if (lower.endsWith('.x_b') || lower.endsWith('.xmt_bin')) {
171
+ return 'parasolid-binary'
172
+ }
173
+ if (
174
+ lower.endsWith('.pdf') ||
175
+ EmbeddedFileInventoryBuilder.#asciiPrefix(bytes).startsWith('%PDF')
176
+ ) {
177
+ return 'pdf'
178
+ }
179
+ if (EmbeddedFileInventoryBuilder.#isLikelyText(bytes)) {
180
+ return 'text'
181
+ }
182
+
183
+ return 'binary'
184
+ }
185
+
186
+ /**
187
+ * Returns true when bytes start with a prefix.
188
+ * @param {Uint8Array} bytes Payload bytes.
189
+ * @param {number[]} prefix Prefix bytes.
190
+ * @returns {boolean}
191
+ */
192
+ static #hasPrefix(bytes, prefix) {
193
+ return prefix.every((value, index) => bytes[index] === value)
194
+ }
195
+
196
+ /**
197
+ * Decodes a short ASCII prefix.
198
+ * @param {Uint8Array} bytes Payload bytes.
199
+ * @returns {string}
200
+ */
201
+ static #asciiPrefix(bytes) {
202
+ return new TextDecoder('latin1').decode(bytes.slice(0, 8))
203
+ }
204
+
205
+ /**
206
+ * Decodes and trims a text probe.
207
+ * @param {Uint8Array} bytes Payload bytes.
208
+ * @returns {string}
209
+ */
210
+ static #trimmedText(bytes) {
211
+ return new TextDecoder('utf-8', { fatal: false })
212
+ .decode(bytes.slice(0, Math.min(bytes.byteLength, 256)))
213
+ .trim()
214
+ }
215
+
216
+ /**
217
+ * Returns true when a payload is printable enough to treat as text.
218
+ * @param {Uint8Array} bytes Payload bytes.
219
+ * @returns {boolean}
220
+ */
221
+ static #isLikelyText(bytes) {
222
+ let printable = 0
223
+ const length = Math.min(bytes.byteLength, 256)
224
+
225
+ for (let index = 0; index < length; index += 1) {
226
+ const value = bytes[index]
227
+ if (
228
+ value === 0x09 ||
229
+ value === 0x0a ||
230
+ value === 0x0d ||
231
+ (value >= 0x20 && value <= 0x7e)
232
+ ) {
233
+ printable += 1
234
+ }
235
+ }
236
+
237
+ return length > 0 && printable / length >= 0.9
238
+ }
239
+
240
+ /**
241
+ * Computes an FNV-1a 32-bit checksum.
242
+ * @param {Uint8Array} bytes Payload bytes.
243
+ * @returns {string}
244
+ */
245
+ static #fnv1a32(bytes) {
246
+ let hash = 0x811c9dc5
247
+
248
+ for (const value of bytes) {
249
+ hash ^= value
250
+ hash = Math.imul(hash, 0x01000193) >>> 0
251
+ }
252
+
253
+ return hash.toString(16).padStart(8, '0')
254
+ }
255
+ }