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
@@ -4,8 +4,10 @@
4
4
 
5
5
  import { CircuitJsonModelSchema } from './CircuitJsonModelSchema.mjs'
6
6
  import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
7
+ import { CircuitJsonModelAdapterPcbElements } from './CircuitJsonModelAdapterPcbElements.mjs'
7
8
 
8
9
  const Primitives = CircuitJsonModelAdapterPrimitives
10
+ const PcbElements = CircuitJsonModelAdapterPcbElements
9
11
 
10
12
  /**
11
13
  * Converts between legacy renderer models and Circuit JSON element arrays.
@@ -175,7 +177,8 @@ export class CircuitJsonModelAdapter {
175
177
  width: Primitives.number(component.width, 0),
176
178
  height: Primitives.number(component.height, 0)
177
179
  },
178
- rotation: Primitives.number(component.rotation, 0)
180
+ rotation: Primitives.number(component.rotation, 0),
181
+ is_box_with_pins: true
179
182
  })
180
183
  }
181
184
 
@@ -204,9 +207,8 @@ export class CircuitJsonModelAdapter {
204
207
  pin.name || pin.designator || pinIndex,
205
208
  String(pinIndex + 1)
206
209
  ),
207
- pin_number: Primitives.string(
208
- pin.pinNumber || pin.designator || pin.name,
209
- String(pinIndex + 1)
210
+ ...CircuitJsonModelAdapter.#pinNumberField(
211
+ pin.pinNumber || pin.designator || pin.name
210
212
  )
211
213
  })
212
214
  circuitJson.push({
@@ -232,7 +234,8 @@ export class CircuitJsonModelAdapter {
232
234
  circuitJson.push({
233
235
  type: 'source_net',
234
236
  source_net_id: sourceNetId,
235
- name: Primitives.string(net.name, `NET_${netIndex + 1}`)
237
+ name: Primitives.string(net.name, `NET_${netIndex + 1}`),
238
+ member_source_group_ids: []
236
239
  })
237
240
  }
238
241
 
@@ -269,7 +272,7 @@ export class CircuitJsonModelAdapter {
269
272
  */
270
273
  static #appendPcb(circuitJson, model, idScope) {
271
274
  const pcb = model.pcb || {}
272
- const componentIds = new Map()
275
+ const componentRefs = new Map()
273
276
  const sourceNetIds = new Map()
274
277
  const boardId = Primitives.id(idScope, ['pcb_board'])
275
278
 
@@ -295,7 +298,8 @@ export class CircuitJsonModelAdapter {
295
298
  name: Primitives.string(
296
299
  net.name || net.netName,
297
300
  `NET_${netIndex + 1}`
298
- )
301
+ ),
302
+ member_source_group_ids: []
299
303
  })
300
304
  }
301
305
 
@@ -310,9 +314,12 @@ export class CircuitJsonModelAdapter {
310
314
  'pcb_component',
311
315
  component.designator || component.name || componentIndex
312
316
  ])
313
- componentIds.set(
317
+ componentRefs.set(
314
318
  Primitives.componentKey(component, componentIndex),
315
- sourceComponentId
319
+ {
320
+ pcbComponentId,
321
+ sourceComponentId
322
+ }
316
323
  )
317
324
  circuitJson.push(
318
325
  CircuitJsonModelAdapter.#sourceComponent(
@@ -345,7 +352,7 @@ export class CircuitJsonModelAdapter {
345
352
  idScope,
346
353
  pad,
347
354
  padIndex,
348
- componentIds,
355
+ componentRefs,
349
356
  sourceNetIds
350
357
  )
351
358
  }
@@ -367,8 +374,7 @@ export class CircuitJsonModelAdapter {
367
374
  circuitJson,
368
375
  idScope,
369
376
  via,
370
- viaIndex,
371
- sourceNetIds
377
+ viaIndex
372
378
  )
373
379
  }
374
380
  }
@@ -415,7 +421,7 @@ export class CircuitJsonModelAdapter {
415
421
  * @param {string} idScope
416
422
  * @param {Record<string, unknown>} pad
417
423
  * @param {number} padIndex
418
- * @param {Map<string, string>} componentIds
424
+ * @param {Map<string, { pcbComponentId: string, sourceComponentId: string }>} componentRefs
419
425
  * @param {Map<string, string>} sourceNetIds
420
426
  * @returns {void}
421
427
  */
@@ -424,12 +430,14 @@ export class CircuitJsonModelAdapter {
424
430
  idScope,
425
431
  pad,
426
432
  padIndex,
427
- componentIds,
433
+ componentRefs,
428
434
  sourceNetIds
429
435
  ) {
436
+ const componentRef =
437
+ componentRefs.get(String(pad.componentIndex)) ||
438
+ componentRefs.get('0')
430
439
  const sourceComponentId =
431
- componentIds.get(String(pad.componentIndex)) ||
432
- componentIds.get('0') ||
440
+ componentRef?.sourceComponentId ||
433
441
  Primitives.id(idScope, ['source_component', 'unassigned'])
434
442
  const sourcePortId = Primitives.sourcePortId(
435
443
  idScope,
@@ -438,69 +446,74 @@ export class CircuitJsonModelAdapter {
438
446
  sourceComponentId
439
447
  )
440
448
  const pcbPortId = Primitives.id(idScope, ['pcb_port', sourcePortId])
441
- const common = {
442
- source_port_id: sourcePortId,
443
- pcb_port_id: pcbPortId,
444
- pcb_component_id: Primitives.id(idScope, [
445
- 'pcb_component',
446
- pad.componentIndex ?? 'unassigned'
447
- ]),
448
- center: Primitives.milPoint(pad.x, pad.y),
449
- layer: Primitives.side(pad.layer),
450
- port_hints: [
451
- Primitives.string(
452
- pad.name || pad.pinName || pad.designator,
453
- String(padIndex + 1)
454
- )
455
- ]
456
- }
457
- const sourceNetId = Primitives.netIdForPrimitive(
458
- idScope,
459
- pad,
460
- sourceNetIds
461
- )
449
+ const pcbComponentId =
450
+ componentRef?.pcbComponentId ||
451
+ Primitives.id(idScope, ['pcb_component', 'unassigned'])
452
+ const center = Primitives.milPoint(pad.x, pad.y)
453
+ const layer = Primitives.side(pad.layer)
454
+ const portHints = [
455
+ Primitives.string(
456
+ pad.name || pad.pinName || pad.designator,
457
+ String(padIndex + 1)
458
+ )
459
+ ]
460
+ Primitives.netIdForPrimitive(idScope, pad, sourceNetIds)
462
461
 
463
462
  circuitJson.push({
464
463
  type: 'source_port',
465
464
  source_port_id: sourcePortId,
466
465
  source_component_id: sourceComponentId,
467
- name: common.port_hints[0],
468
- pin_number: common.port_hints[0]
466
+ name: portHints[0],
467
+ port_hints: portHints,
468
+ ...CircuitJsonModelAdapter.#pinNumberField(portHints[0])
469
469
  })
470
470
  circuitJson.push({
471
471
  type: 'pcb_port',
472
- ...common,
473
- source_net_id: sourceNetId
472
+ pcb_port_id: pcbPortId,
473
+ source_port_id: sourcePortId,
474
+ pcb_component_id: pcbComponentId,
475
+ x: center.x,
476
+ y: center.y,
477
+ layers: Primitives.isThroughHolePad(pad)
478
+ ? ['top', 'bottom']
479
+ : [layer]
474
480
  })
475
481
 
476
482
  if (Primitives.isThroughHolePad(pad)) {
477
- circuitJson.push({
478
- type: pad.isPlated === false ? 'pcb_hole' : 'pcb_plated_hole',
479
- ...common,
480
- outer_diameter: Primitives.milNumber(
481
- pad.sizeTopX || pad.sizeX || pad.diameter,
482
- 0
483
- ),
484
- hole_diameter: Primitives.milNumber(pad.holeDiameter, 0),
485
- shape: Primitives.padShape(pad)
486
- })
483
+ circuitJson.push(
484
+ pad.isPlated === false
485
+ ? PcbElements.hole(
486
+ idScope,
487
+ pad,
488
+ padIndex,
489
+ pcbComponentId,
490
+ center
491
+ )
492
+ : PcbElements.platedHole(
493
+ idScope,
494
+ pad,
495
+ padIndex,
496
+ pcbComponentId,
497
+ pcbPortId,
498
+ center,
499
+ portHints
500
+ )
501
+ )
487
502
  return
488
503
  }
489
504
 
490
- circuitJson.push({
491
- type: 'pcb_smtpad',
492
- ...common,
493
- shape: Primitives.padShape(pad),
494
- width: Primitives.milNumber(
495
- pad.sizeTopX || pad.sizeX || pad.width,
496
- 0
497
- ),
498
- height: Primitives.milNumber(
499
- pad.sizeTopY || pad.sizeY || pad.height,
500
- 0
501
- ),
502
- rotation: Primitives.number(pad.rotation || pad.holeRotation, 0)
503
- })
505
+ circuitJson.push(
506
+ PcbElements.smtPad(
507
+ idScope,
508
+ pad,
509
+ padIndex,
510
+ pcbComponentId,
511
+ pcbPortId,
512
+ center,
513
+ layer,
514
+ portHints
515
+ )
516
+ )
504
517
  }
505
518
 
506
519
  /**
@@ -523,14 +536,16 @@ export class CircuitJsonModelAdapter {
523
536
  'source_trace',
524
537
  track.netName || track.netIndex || trackIndex
525
538
  ])
539
+ const sourceNetId = Primitives.netIdForPrimitive(
540
+ idScope,
541
+ track,
542
+ sourceNetIds
543
+ )
526
544
  circuitJson.push({
527
545
  type: 'source_trace',
528
546
  source_trace_id: sourceTraceId,
529
- source_net_id: Primitives.netIdForPrimitive(
530
- idScope,
531
- track,
532
- sourceNetIds
533
- )
547
+ connected_source_port_ids: [],
548
+ connected_source_net_ids: sourceNetId ? [sourceNetId] : []
534
549
  })
535
550
  circuitJson.push({
536
551
  type: 'pcb_trace',
@@ -561,18 +576,12 @@ export class CircuitJsonModelAdapter {
561
576
  * @param {string} idScope
562
577
  * @param {Record<string, unknown>} via
563
578
  * @param {number} viaIndex
564
- * @param {Map<string, string>} sourceNetIds
565
579
  * @returns {void}
566
580
  */
567
- static #appendPcbVia(circuitJson, idScope, via, viaIndex, sourceNetIds) {
581
+ static #appendPcbVia(circuitJson, idScope, via, viaIndex) {
568
582
  circuitJson.push({
569
583
  type: 'pcb_via',
570
584
  pcb_via_id: Primitives.id(idScope, ['pcb_via', viaIndex]),
571
- source_net_id: Primitives.netIdForPrimitive(
572
- idScope,
573
- via,
574
- sourceNetIds
575
- ),
576
585
  x: Primitives.milNumber(via.x, 0),
577
586
  y: Primitives.milNumber(via.y, 0),
578
587
  outer_diameter: Primitives.milNumber(via.diameter, 0),
@@ -664,33 +673,47 @@ export class CircuitJsonModelAdapter {
664
673
  x2: Primitives.number(line.x2, 0),
665
674
  y2: Primitives.number(line.y2, 0),
666
675
  stroke_width: Primitives.number(line.width, 1),
667
- is_dashed: line.dashed === true
676
+ is_dashed: line.dashed === true,
677
+ color: '#000000'
668
678
  }
669
679
  circuitJson.push(lineElement)
670
680
 
671
681
  if (line.kind === 'wire' || line.netName || line.netIndex) {
682
+ const sourceNetId =
683
+ netIds.get(String(line.netName)) ||
684
+ Primitives.sourceNetId(
685
+ idScope,
686
+ line.netName || line.netIndex || lineIndex
687
+ )
688
+ const sourceTraceId = Primitives.id(idScope, [
689
+ 'source_trace',
690
+ line.netName || line.netIndex || lineIndex,
691
+ lineIndex
692
+ ])
693
+ circuitJson.push({
694
+ type: 'source_trace',
695
+ source_trace_id: sourceTraceId,
696
+ connected_source_port_ids: [],
697
+ connected_source_net_ids: sourceNetId ? [sourceNetId] : []
698
+ })
672
699
  circuitJson.push({
673
700
  type: 'schematic_trace',
674
701
  schematic_trace_id: Primitives.id(idScope, [
675
702
  'schematic_trace',
676
703
  lineIndex
677
704
  ]),
678
- source_trace_id: Primitives.id(idScope, [
679
- 'source_trace',
680
- line.netName || line.netIndex || lineIndex
681
- ]),
682
- source_net_id:
683
- netIds.get(String(line.netName)) ||
684
- Primitives.sourceNetId(
685
- idScope,
686
- line.netName || line.netIndex || lineIndex
687
- ),
705
+ source_trace_id: sourceTraceId,
706
+ junctions: [],
688
707
  edges: [
689
708
  {
690
- x1: lineElement.x1,
691
- y1: lineElement.y1,
692
- x2: lineElement.x2,
693
- y2: lineElement.y2
709
+ from: {
710
+ x: lineElement.x1,
711
+ y: lineElement.y1
712
+ },
713
+ to: {
714
+ x: lineElement.x2,
715
+ y: lineElement.y2
716
+ }
694
717
  }
695
718
  ]
696
719
  })
@@ -712,11 +735,11 @@ export class CircuitJsonModelAdapter {
712
735
  )
713
736
  const base = {
714
737
  text: textValue,
715
- anchor_position: Primitives.point(text.x, text.y),
716
- anchor_alignment: 'center'
738
+ position: Primitives.point(text.x, text.y)
717
739
  }
718
740
 
719
741
  if (Primitives.isNetLabel(text)) {
742
+ const center = Primitives.point(text.x, text.y)
720
743
  circuitJson.push({
721
744
  type: 'schematic_net_label',
722
745
  schematic_net_label_id: Primitives.id(idScope, [
@@ -727,7 +750,10 @@ export class CircuitJsonModelAdapter {
727
750
  idScope,
728
751
  textValue || textIndex
729
752
  ),
730
- ...base
753
+ text: textValue,
754
+ center,
755
+ anchor_position: center,
756
+ anchor_side: 'top'
731
757
  })
732
758
  return
733
759
  }
@@ -738,10 +764,24 @@ export class CircuitJsonModelAdapter {
738
764
  'schematic_text',
739
765
  textIndex
740
766
  ]),
741
- ...base
767
+ ...base,
768
+ font_size: Primitives.number(text.fontSize || text.size, 0.18),
769
+ rotation: Primitives.number(text.rotation, 0),
770
+ anchor: 'center',
771
+ color: '#000000'
742
772
  })
743
773
  }
744
774
 
775
+ /**
776
+ * Returns an optional numeric pin number field.
777
+ * @param {unknown} value
778
+ * @returns {{ pin_number?: number }}
779
+ */
780
+ static #pinNumberField(value) {
781
+ const pinNumber = Primitives.number(value, null)
782
+ return pinNumber === null ? {} : { pin_number: pinNumber }
783
+ }
784
+
745
785
  /**
746
786
  * Returns a source component id for one schematic pin.
747
787
  * @param {Record<string, unknown>} pin
@@ -0,0 +1,244 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
6
+
7
+ const Primitives = CircuitJsonModelAdapterPrimitives
8
+
9
+ /**
10
+ * Builds upstream-compatible Circuit JSON PCB element records.
11
+ */
12
+ export class CircuitJsonModelAdapterPcbElements {
13
+ /**
14
+ * Builds an upstream-compatible SMT pad element.
15
+ * @param {string} idScope
16
+ * @param {Record<string, unknown>} pad
17
+ * @param {number} padIndex
18
+ * @param {string} pcbComponentId
19
+ * @param {string} pcbPortId
20
+ * @param {{ x: number, y: number }} center
21
+ * @param {string} layer
22
+ * @param {string[]} portHints
23
+ * @returns {object}
24
+ */
25
+ static smtPad(
26
+ idScope,
27
+ pad,
28
+ padIndex,
29
+ pcbComponentId,
30
+ pcbPortId,
31
+ center,
32
+ layer,
33
+ portHints
34
+ ) {
35
+ const shape = Primitives.padShape(pad)
36
+ const width = Primitives.milNumber(
37
+ pad.sizeTopX || pad.sizeX || pad.width,
38
+ 0
39
+ )
40
+ const height = Primitives.milNumber(
41
+ pad.sizeTopY || pad.sizeY || pad.height,
42
+ 0
43
+ )
44
+ const rotation = Primitives.number(pad.rotation || pad.holeRotation, 0)
45
+ const base = {
46
+ type: 'pcb_smtpad',
47
+ pcb_smtpad_id: Primitives.id(idScope, ['pcb_smtpad', padIndex]),
48
+ pcb_component_id: pcbComponentId,
49
+ pcb_port_id: pcbPortId,
50
+ x: center.x,
51
+ y: center.y,
52
+ layer,
53
+ port_hints: portHints
54
+ }
55
+
56
+ if (shape === 'circle') {
57
+ return {
58
+ ...base,
59
+ shape,
60
+ radius: Primitives.round(Math.max(width, height) / 2)
61
+ }
62
+ }
63
+
64
+ if (shape === 'pill') {
65
+ return {
66
+ ...base,
67
+ shape: CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
68
+ ? 'rotated_pill'
69
+ : 'pill',
70
+ width,
71
+ height,
72
+ radius: Primitives.round(Math.min(width, height) / 2),
73
+ ...CircuitJsonModelAdapterPcbElements.#ccwRotationField(
74
+ rotation
75
+ )
76
+ }
77
+ }
78
+
79
+ return {
80
+ ...base,
81
+ shape: CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
82
+ ? 'rotated_rect'
83
+ : 'rect',
84
+ width,
85
+ height,
86
+ ...CircuitJsonModelAdapterPcbElements.#ccwRotationField(rotation)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Builds an upstream-compatible non-plated hole element.
92
+ * @param {string} idScope
93
+ * @param {Record<string, unknown>} pad
94
+ * @param {number} padIndex
95
+ * @param {string} pcbComponentId
96
+ * @param {{ x: number, y: number }} center
97
+ * @returns {object}
98
+ */
99
+ static hole(idScope, pad, padIndex, pcbComponentId, center) {
100
+ const shape = Primitives.padShape(pad)
101
+ const width = Primitives.milNumber(
102
+ pad.sizeTopX || pad.sizeX || pad.width || pad.diameter,
103
+ 0
104
+ )
105
+ const height = Primitives.milNumber(
106
+ pad.sizeTopY || pad.sizeY || pad.height || pad.diameter,
107
+ 0
108
+ )
109
+ const holeDiameter = Primitives.milNumber(pad.holeDiameter, 0)
110
+ const base = {
111
+ type: 'pcb_hole',
112
+ pcb_hole_id: Primitives.id(idScope, ['pcb_hole', padIndex]),
113
+ pcb_component_id: pcbComponentId,
114
+ x: center.x,
115
+ y: center.y
116
+ }
117
+
118
+ if (shape === 'circle') {
119
+ return {
120
+ ...base,
121
+ hole_shape: 'circle',
122
+ hole_diameter: holeDiameter
123
+ }
124
+ }
125
+
126
+ return {
127
+ ...base,
128
+ hole_shape: shape === 'pill' ? 'pill' : 'rect',
129
+ hole_width: holeDiameter || width,
130
+ hole_height: holeDiameter || height
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Builds an upstream-compatible plated hole element.
136
+ * @param {string} idScope
137
+ * @param {Record<string, unknown>} pad
138
+ * @param {number} padIndex
139
+ * @param {string} pcbComponentId
140
+ * @param {string} pcbPortId
141
+ * @param {{ x: number, y: number }} center
142
+ * @param {string[]} portHints
143
+ * @returns {object}
144
+ */
145
+ static platedHole(
146
+ idScope,
147
+ pad,
148
+ padIndex,
149
+ pcbComponentId,
150
+ pcbPortId,
151
+ center,
152
+ portHints
153
+ ) {
154
+ const shape = Primitives.padShape(pad)
155
+ const width = Primitives.milNumber(
156
+ pad.sizeTopX || pad.sizeX || pad.width || pad.diameter,
157
+ 0
158
+ )
159
+ const height = Primitives.milNumber(
160
+ pad.sizeTopY || pad.sizeY || pad.height || pad.diameter,
161
+ 0
162
+ )
163
+ const holeDiameter = Primitives.milNumber(pad.holeDiameter, 0)
164
+ const rotation = Primitives.number(pad.rotation || pad.holeRotation, 0)
165
+ const base = {
166
+ type: 'pcb_plated_hole',
167
+ pcb_plated_hole_id: Primitives.id(idScope, [
168
+ 'pcb_plated_hole',
169
+ padIndex
170
+ ]),
171
+ pcb_component_id: pcbComponentId,
172
+ pcb_port_id: pcbPortId,
173
+ x: center.x,
174
+ y: center.y,
175
+ layers: ['top', 'bottom'],
176
+ port_hints: portHints
177
+ }
178
+
179
+ if (shape === 'circle') {
180
+ return {
181
+ ...base,
182
+ shape,
183
+ outer_diameter: Primitives.round(Math.max(width, height)),
184
+ hole_diameter: holeDiameter
185
+ }
186
+ }
187
+
188
+ if (shape === 'pill') {
189
+ return {
190
+ ...base,
191
+ shape,
192
+ outer_width: width,
193
+ outer_height: height,
194
+ hole_width: holeDiameter,
195
+ hole_height: holeDiameter,
196
+ ccw_rotation: rotation
197
+ }
198
+ }
199
+
200
+ return {
201
+ ...base,
202
+ shape: 'circular_hole_with_rect_pad',
203
+ hole_shape: 'circle',
204
+ pad_shape: 'rect',
205
+ hole_diameter: holeDiameter,
206
+ rect_pad_width: width,
207
+ rect_pad_height: height,
208
+ ...CircuitJsonModelAdapterPcbElements.#rectCcwRotationField(
209
+ rotation
210
+ )
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returns true when a rotation value should use a rotated pad shape.
216
+ * @param {number | null} rotation
217
+ * @returns {boolean}
218
+ */
219
+ static #hasRotation(rotation) {
220
+ return Math.abs(rotation || 0) > 0.000001
221
+ }
222
+
223
+ /**
224
+ * Returns an optional counter-clockwise rotation field.
225
+ * @param {number | null} rotation
226
+ * @returns {{ ccw_rotation?: number }}
227
+ */
228
+ static #ccwRotationField(rotation) {
229
+ return CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
230
+ ? { ccw_rotation: rotation || 0 }
231
+ : {}
232
+ }
233
+
234
+ /**
235
+ * Returns an optional rectangular pad rotation field.
236
+ * @param {number | null} rotation
237
+ * @returns {{ rect_ccw_rotation?: number }}
238
+ */
239
+ static #rectCcwRotationField(rotation) {
240
+ return CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
241
+ ? { rect_ccw_rotation: rotation || 0 }
242
+ : {}
243
+ }
244
+ }
@@ -8,7 +8,7 @@
8
8
  export class CircuitJsonModelSchema {
9
9
  static CURRENT_SCHEMA_ID = 'https://github.com/tscircuit/circuit-json'
10
10
 
11
- static CURRENT_SCHEMA_VERSION = '0.0.431'
11
+ static CURRENT_SCHEMA_VERSION = '0.0.433'
12
12
 
13
13
  static FORMAT_NAME = 'circuit-json'
14
14
 
@@ -30,6 +30,7 @@ export class OleCompoundDocument {
30
30
  constructor(arrayBuffer) {
31
31
  this.#reader = new BinaryReader(arrayBuffer)
32
32
  this.#header = this.#parseHeader()
33
+ this.#assertSectorAlignedFile()
33
34
  this.#fatEntries = this.#parseFatEntries()
34
35
  this.#directoryEntries = this.#parseDirectoryEntries()
35
36
  this.#miniFatEntries = this.#parseMiniFatEntries()
@@ -137,6 +138,25 @@ export class OleCompoundDocument {
137
138
  }
138
139
  }
139
140
 
141
+ /**
142
+ * Ensures the byte stream still matches the OLE sector grid.
143
+ */
144
+ #assertSectorAlignedFile() {
145
+ const payloadByteLength =
146
+ this.#reader.byteLength - OleConstants.HEADER_BYTE_LENGTH
147
+
148
+ if (
149
+ payloadByteLength < 0 ||
150
+ payloadByteLength % this.#header.sectorByteLength !== 0
151
+ ) {
152
+ throw new Error(
153
+ 'OLE compound document byte length is not sector-aligned. ' +
154
+ 'The file may be truncated or line-ending normalized; ' +
155
+ 'keep Altium binary files committed and served as binary.'
156
+ )
157
+ }
158
+ }
159
+
140
160
  /**
141
161
  * Parses all FAT entries.
142
162
  * @returns {number[]}