circuit-json-to-tscircuit 0.0.83 → 0.0.85

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.
@@ -1020,6 +1020,19 @@ var convertComponents = (circuitJson) => su3(circuitJson).schematic_component.li
1020
1020
  return schematicComponent.is_box_with_pins ? convertBoxComponent({ circuitJson, schematicComponent }) : [];
1021
1021
  });
1022
1022
 
1023
+ // lib/generate-board-schematic-tsx/convert-schematic-graphics.ts
1024
+ var convertSchematicGraphics = (circuitJson) => circuitJson.flatMap((element) => {
1025
+ if (element.type !== "schematic_graphic") return [];
1026
+ return [
1027
+ formatElement("schematicgraphic", {
1028
+ imageUrl: element.asset?.url,
1029
+ svgContent: element.svg_content,
1030
+ width: element.width,
1031
+ height: element.height
1032
+ })
1033
+ ];
1034
+ });
1035
+
1023
1036
  // lib/generate-board-schematic-tsx/convert-net-labels.ts
1024
1037
  import { su as su4 } from "@tscircuit/soup-util";
1025
1038
  import { schematic_text as schematic_text3 } from "circuit-json";
@@ -1113,7 +1126,8 @@ var generateBoardSchematicElements = (circuitJson) => {
1113
1126
  ...convertComponents(circuitJson),
1114
1127
  ...convertNetLabels(circuitJson),
1115
1128
  ...convertPrimitives(circuitJson),
1116
- ...convertTraces(circuitJson)
1129
+ ...convertTraces(circuitJson),
1130
+ ...convertSchematicGraphics(circuitJson)
1117
1131
  ];
1118
1132
  };
1119
1133
 
@@ -1628,20 +1642,91 @@ var convertHoles = (circuitJson) => {
1628
1642
 
1629
1643
  // lib/generate-footprint-tsx/convert-keepouts.ts
1630
1644
  import { su as su12 } from "@tscircuit/soup-util";
1645
+
1646
+ // lib/generate-footprint-tsx/get-filled-circle-from-keepout-outline.ts
1647
+ var MINIMUM_CIRCLE_VERTEX_COUNT = 8;
1648
+ var CIRCLE_RADIUS_TOLERANCE = 1e-3;
1649
+ var MAXIMUM_ANGULAR_STEP_RATIO = 1.5;
1650
+ var FULL_ROTATION_RADIANS = Math.PI * 2;
1651
+ var getNormalizedAngleDelta = (angleDelta) => {
1652
+ if (angleDelta > Math.PI) return angleDelta - FULL_ROTATION_RADIANS;
1653
+ if (angleDelta <= -Math.PI) return angleDelta + FULL_ROTATION_RADIANS;
1654
+ return angleDelta;
1655
+ };
1656
+ var completesCircularTraversal = ({
1657
+ center,
1658
+ outline,
1659
+ radius
1660
+ }) => {
1661
+ const firstPoint = outline[0];
1662
+ const lastPoint = outline.at(-1);
1663
+ if (!firstPoint || !lastPoint) return false;
1664
+ if (Math.hypot(firstPoint.x - lastPoint.x, firstPoint.y - lastPoint.y) > radius * CIRCLE_RADIUS_TOLERANCE) {
1665
+ return false;
1666
+ }
1667
+ const traversalPoints = outline.slice(0, -1);
1668
+ const maximumAngularStep = FULL_ROTATION_RADIANS / traversalPoints.length * MAXIMUM_ANGULAR_STEP_RATIO;
1669
+ const angleDeltas = traversalPoints.map((point, pointIndex) => {
1670
+ const nextPoint = traversalPoints[(pointIndex + 1) % traversalPoints.length];
1671
+ const pointAngle = Math.atan2(point.y - center.y, point.x - center.x);
1672
+ const nextPointAngle = Math.atan2(
1673
+ nextPoint.y - center.y,
1674
+ nextPoint.x - center.x
1675
+ );
1676
+ return getNormalizedAngleDelta(nextPointAngle - pointAngle);
1677
+ });
1678
+ const traversalDirection = Math.sign(angleDeltas[0] ?? 0);
1679
+ return traversalDirection !== 0 && angleDeltas.every(
1680
+ (angleDelta) => Math.sign(angleDelta) === traversalDirection && Math.abs(angleDelta) <= maximumAngularStep
1681
+ );
1682
+ };
1683
+ var getFilledCircleFromKeepoutOutline = (keepout) => {
1684
+ const outline = keepout.outline;
1685
+ if (outline.length < MINIMUM_CIRCLE_VERTEX_COUNT) return null;
1686
+ const minX = Math.min(...outline.map((point) => point.x));
1687
+ const maxX = Math.max(...outline.map((point) => point.x));
1688
+ const minY = Math.min(...outline.map((point) => point.y));
1689
+ const maxY = Math.max(...outline.map((point) => point.y));
1690
+ const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1691
+ const horizontalRadius = (maxX - minX) / 2;
1692
+ const verticalRadius = (maxY - minY) / 2;
1693
+ const centerlineRadius = (horizontalRadius + verticalRadius) / 2;
1694
+ if (!Number.isFinite(centerlineRadius) || centerlineRadius <= 0) return null;
1695
+ if (Math.abs(horizontalRadius - verticalRadius) > centerlineRadius * CIRCLE_RADIUS_TOLERANCE) {
1696
+ return null;
1697
+ }
1698
+ const isCircular = outline.every((point) => {
1699
+ const pointRadius = Math.hypot(point.x - center.x, point.y - center.y);
1700
+ return Math.abs(pointRadius - centerlineRadius) <= centerlineRadius * CIRCLE_RADIUS_TOLERANCE;
1701
+ });
1702
+ if (!isCircular) return null;
1703
+ if (!completesCircularTraversal({ center, outline, radius: centerlineRadius }))
1704
+ return null;
1705
+ const strokeRadius = keepout.stroke_width / 2;
1706
+ if (strokeRadius < centerlineRadius) return null;
1707
+ return { center, radius: centerlineRadius + strokeRadius };
1708
+ };
1709
+
1710
+ // lib/generate-footprint-tsx/convert-keepouts.ts
1711
+ var formatLayersAttribute = (layers) => ` layers={${JSON.stringify(layers)}}`;
1631
1712
  var convertKeepouts = (circuitJson) => {
1632
1713
  const pcbKeepouts = su12(circuitJson).pcb_keepout.list();
1633
1714
  const elementStrings = [];
1634
1715
  for (const keepout of pcbKeepouts) {
1635
1716
  if (keepout.shape === "rect") {
1636
1717
  elementStrings.push(
1637
- `<keepout shape="rect" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" width="${formatMm(keepout.width)}" height="${formatMm(keepout.height)}" />`
1718
+ `<keepout shape="rect" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" width="${formatMm(keepout.width)}" height="${formatMm(keepout.height)}"${formatLayersAttribute(keepout.layers)} />`
1638
1719
  );
1639
1720
  } else if (keepout.shape === "circle") {
1640
1721
  elementStrings.push(
1641
- `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}" />`
1722
+ `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}"${formatLayersAttribute(keepout.layers)} />`
1723
+ );
1724
+ } else if (keepout.shape === "outline") {
1725
+ const circle = getFilledCircleFromKeepoutOutline(keepout);
1726
+ if (!circle) continue;
1727
+ elementStrings.push(
1728
+ `<keepout shape="circle" pcbX="${formatMm(circle.center.x)}" pcbY="${formatMm(circle.center.y)}" radius="${formatMm(circle.radius)}"${formatLayersAttribute(keepout.layers)} />`
1642
1729
  );
1643
- } else {
1644
- console.warn(`Unhandled pcb_keepout shape: ${keepout.shape}`);
1645
1730
  }
1646
1731
  }
1647
1732
  return elementStrings;
package/dist/cli/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  convertCircuitJsonToTscircuit
4
- } from "../chunk-B56TEDTF.js";
4
+ } from "../chunk-UZVNOAO4.js";
5
5
 
6
6
  // cli/main.ts
7
7
  import { Command } from "commander";
@@ -16,7 +16,7 @@ var package_default = {
16
16
  "./cli": "./dist/cli/main.js"
17
17
  },
18
18
  type: "module",
19
- version: "0.0.82",
19
+ version: "0.0.84",
20
20
  repository: {
21
21
  type: "git",
22
22
  url: "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
@@ -38,7 +38,7 @@ var package_default = {
38
38
  "@types/react-dom": "18",
39
39
  "bun-match-svg": "^0.0.15",
40
40
  "circuit-json": "^0.0.507",
41
- "circuit-to-svg": "^0.0.442",
41
+ "circuit-to-svg": "https://pkg.pr.new/tscircuit/circuit-to-svg@738ef1d",
42
42
  commander: "^13.1.0",
43
43
  prettier: "^3.9.5",
44
44
  "schematic-symbols": "0.0.248",
package/dist/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertCircuitJsonToTscircuit
3
- } from "../chunk-B56TEDTF.js";
3
+ } from "../chunk-UZVNOAO4.js";
4
4
  export {
5
5
  convertCircuitJsonToTscircuit
6
6
  };
@@ -0,0 +1,18 @@
1
+ import type { SchematicElementConverter } from "./converter-types"
2
+ import { formatElement } from "./format-attributes"
3
+
4
+ export const convertSchematicGraphics: SchematicElementConverter = (
5
+ circuitJson,
6
+ ) =>
7
+ circuitJson.flatMap((element) => {
8
+ if (element.type !== "schematic_graphic") return []
9
+
10
+ return [
11
+ formatElement("schematicgraphic", {
12
+ imageUrl: element.asset?.url,
13
+ svgContent: element.svg_content,
14
+ width: element.width,
15
+ height: element.height,
16
+ }),
17
+ ]
18
+ })
@@ -1,6 +1,7 @@
1
1
  import type { AnyCircuitElement } from "circuit-json"
2
2
  import { convertPrimitives } from "./generate-board-schematic-tsx/convert-primitives"
3
3
  import { convertComponents } from "./generate-board-schematic-tsx/convert-components"
4
+ import { convertSchematicGraphics } from "./generate-board-schematic-tsx/convert-schematic-graphics"
4
5
  import { convertNetLabels } from "./generate-board-schematic-tsx/convert-net-labels"
5
6
  import { convertTraces } from "./generate-board-schematic-tsx/convert-traces"
6
7
 
@@ -21,5 +22,6 @@ export const generateBoardSchematicElements = (
21
22
  ...convertNetLabels(circuitJson),
22
23
  ...convertPrimitives(circuitJson),
23
24
  ...convertTraces(circuitJson),
25
+ ...convertSchematicGraphics(circuitJson),
24
26
  ]
25
27
  }
@@ -1,6 +1,10 @@
1
1
  import { su } from "@tscircuit/soup-util"
2
2
  import type { FootprintElementConverter } from "./converter-types"
3
3
  import { formatMm } from "./footprint-tsx-attribute-formatters/format-mm"
4
+ import { getFilledCircleFromKeepoutOutline } from "./get-filled-circle-from-keepout-outline"
5
+
6
+ const formatLayersAttribute = (layers: string[]): string =>
7
+ ` layers={${JSON.stringify(layers)}}`
4
8
 
5
9
  export const convertKeepouts: FootprintElementConverter = (circuitJson) => {
6
10
  const pcbKeepouts = su(circuitJson).pcb_keepout.list()
@@ -9,14 +13,18 @@ export const convertKeepouts: FootprintElementConverter = (circuitJson) => {
9
13
  for (const keepout of pcbKeepouts) {
10
14
  if (keepout.shape === "rect") {
11
15
  elementStrings.push(
12
- `<keepout shape="rect" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" width="${formatMm(keepout.width)}" height="${formatMm(keepout.height)}" />`,
16
+ `<keepout shape="rect" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" width="${formatMm(keepout.width)}" height="${formatMm(keepout.height)}"${formatLayersAttribute(keepout.layers)} />`,
13
17
  )
14
18
  } else if (keepout.shape === "circle") {
15
19
  elementStrings.push(
16
- `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}" />`,
20
+ `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}"${formatLayersAttribute(keepout.layers)} />`,
21
+ )
22
+ } else if (keepout.shape === "outline") {
23
+ const circle = getFilledCircleFromKeepoutOutline(keepout)
24
+ if (!circle) continue
25
+ elementStrings.push(
26
+ `<keepout shape="circle" pcbX="${formatMm(circle.center.x)}" pcbY="${formatMm(circle.center.y)}" radius="${formatMm(circle.radius)}"${formatLayersAttribute(keepout.layers)} />`,
17
27
  )
18
- } else {
19
- console.warn(`Unhandled pcb_keepout shape: ${(keepout as any).shape}`)
20
28
  }
21
29
  }
22
30
 
@@ -0,0 +1,104 @@
1
+ import type { PcbKeepoutOutline } from "circuit-json"
2
+
3
+ interface FilledKeepoutCircle {
4
+ center: { x: number; y: number }
5
+ radius: number
6
+ }
7
+
8
+ const MINIMUM_CIRCLE_VERTEX_COUNT = 8
9
+ const CIRCLE_RADIUS_TOLERANCE = 0.001
10
+ const MAXIMUM_ANGULAR_STEP_RATIO = 1.5
11
+ const FULL_ROTATION_RADIANS = Math.PI * 2
12
+
13
+ const getNormalizedAngleDelta = (angleDelta: number): number => {
14
+ if (angleDelta > Math.PI) return angleDelta - FULL_ROTATION_RADIANS
15
+ if (angleDelta <= -Math.PI) return angleDelta + FULL_ROTATION_RADIANS
16
+ return angleDelta
17
+ }
18
+
19
+ const completesCircularTraversal = ({
20
+ center,
21
+ outline,
22
+ radius,
23
+ }: {
24
+ center: { x: number; y: number }
25
+ outline: PcbKeepoutOutline["outline"]
26
+ radius: number
27
+ }): boolean => {
28
+ const firstPoint = outline[0]
29
+ const lastPoint = outline.at(-1)
30
+ if (!firstPoint || !lastPoint) return false
31
+ if (
32
+ Math.hypot(firstPoint.x - lastPoint.x, firstPoint.y - lastPoint.y) >
33
+ radius * CIRCLE_RADIUS_TOLERANCE
34
+ ) {
35
+ return false
36
+ }
37
+
38
+ const traversalPoints = outline.slice(0, -1)
39
+ const maximumAngularStep =
40
+ (FULL_ROTATION_RADIANS / traversalPoints.length) *
41
+ MAXIMUM_ANGULAR_STEP_RATIO
42
+ const angleDeltas = traversalPoints.map((point, pointIndex) => {
43
+ const nextPoint =
44
+ traversalPoints[(pointIndex + 1) % traversalPoints.length]!
45
+ const pointAngle = Math.atan2(point.y - center.y, point.x - center.x)
46
+ const nextPointAngle = Math.atan2(
47
+ nextPoint.y - center.y,
48
+ nextPoint.x - center.x,
49
+ )
50
+ return getNormalizedAngleDelta(nextPointAngle - pointAngle)
51
+ })
52
+ const traversalDirection = Math.sign(angleDeltas[0] ?? 0)
53
+
54
+ return (
55
+ traversalDirection !== 0 &&
56
+ angleDeltas.every(
57
+ (angleDelta) =>
58
+ Math.sign(angleDelta) === traversalDirection &&
59
+ Math.abs(angleDelta) <= maximumAngularStep,
60
+ )
61
+ )
62
+ }
63
+
64
+ export const getFilledCircleFromKeepoutOutline = (
65
+ keepout: PcbKeepoutOutline,
66
+ ): FilledKeepoutCircle | null => {
67
+ const outline = keepout.outline
68
+ if (outline.length < MINIMUM_CIRCLE_VERTEX_COUNT) return null
69
+
70
+ const minX = Math.min(...outline.map((point) => point.x))
71
+ const maxX = Math.max(...outline.map((point) => point.x))
72
+ const minY = Math.min(...outline.map((point) => point.y))
73
+ const maxY = Math.max(...outline.map((point) => point.y))
74
+ const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }
75
+ const horizontalRadius = (maxX - minX) / 2
76
+ const verticalRadius = (maxY - minY) / 2
77
+ const centerlineRadius = (horizontalRadius + verticalRadius) / 2
78
+
79
+ if (!Number.isFinite(centerlineRadius) || centerlineRadius <= 0) return null
80
+ if (
81
+ Math.abs(horizontalRadius - verticalRadius) >
82
+ centerlineRadius * CIRCLE_RADIUS_TOLERANCE
83
+ ) {
84
+ return null
85
+ }
86
+
87
+ const isCircular = outline.every((point) => {
88
+ const pointRadius = Math.hypot(point.x - center.x, point.y - center.y)
89
+ return (
90
+ Math.abs(pointRadius - centerlineRadius) <=
91
+ centerlineRadius * CIRCLE_RADIUS_TOLERANCE
92
+ )
93
+ })
94
+ if (!isCircular) return null
95
+ if (
96
+ !completesCircularTraversal({ center, outline, radius: centerlineRadius })
97
+ )
98
+ return null
99
+
100
+ const strokeRadius = keepout.stroke_width / 2
101
+ if (strokeRadius < centerlineRadius) return null
102
+
103
+ return { center, radius: centerlineRadius + strokeRadius }
104
+ }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "./cli": "./dist/cli/main.js"
7
7
  },
8
8
  "type": "module",
9
- "version": "0.0.83",
9
+ "version": "0.0.85",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
@@ -28,7 +28,7 @@
28
28
  "@types/react-dom": "18",
29
29
  "bun-match-svg": "^0.0.15",
30
30
  "circuit-json": "^0.0.507",
31
- "circuit-to-svg": "^0.0.442",
31
+ "circuit-to-svg": "https://pkg.pr.new/tscircuit/circuit-to-svg@738ef1d",
32
32
  "commander": "^13.1.0",
33
33
  "prettier": "^3.9.5",
34
34
  "schematic-symbols": "0.0.248",
@@ -13,3 +13,7 @@ LMG342X-BB-EVM PCB fixtures were resolved from their matching `.PrjPcb` files us
13
13
  The upstream files and checksums are maintained in that repository's
14
14
  `scripts/references/reference-manifest.ts`. The gzip files only avoid committing
15
15
  several megabytes of repetitive JSON; tests expand them before conversion.
16
+
17
+ `tmds62levm-sheet-05.circuit.json.gz` contains the unchanged conversion of
18
+ TMDS62LEVM revision B sheet 05 from `altium-to-circuit-json` v0.0.85
19
+ (`568824fb3ff64992133378b30e88fc23bbd43f48`).
@@ -0,0 +1,49 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { PcbKeepoutOutline } from "circuit-json"
3
+ import { getFilledCircleFromKeepoutOutline } from "../lib/generate-footprint-tsx/get-filled-circle-from-keepout-outline"
4
+
5
+ const createCircularKeepout = ({
6
+ closeOutline,
7
+ sweepDegrees,
8
+ }: {
9
+ closeOutline: boolean
10
+ sweepDegrees: number
11
+ }): PcbKeepoutOutline => {
12
+ const segmentCount = 48
13
+ const outline = Array.from({ length: segmentCount + 1 }, (_, pointIndex) => {
14
+ const angle = ((pointIndex / segmentCount) * sweepDegrees * Math.PI) / 180
15
+ return { x: Math.cos(angle), y: Math.sin(angle) }
16
+ })
17
+ if (closeOutline && sweepDegrees < 360) outline.push(outline[0]!)
18
+
19
+ return {
20
+ type: "pcb_keepout",
21
+ pcb_keepout_id: "circular_outline_keepout",
22
+ shape: "outline",
23
+ outline,
24
+ stroke_width: 2,
25
+ layers: ["top"],
26
+ }
27
+ }
28
+
29
+ test("only complete closed circular outlines become filled disks", () => {
30
+ const fullCircle = createCircularKeepout({
31
+ closeOutline: true,
32
+ sweepDegrees: 360,
33
+ })
34
+ const openPartialArc = createCircularKeepout({
35
+ closeOutline: false,
36
+ sweepDegrees: 270,
37
+ })
38
+ const closedPartialArc = createCircularKeepout({
39
+ closeOutline: true,
40
+ sweepDegrees: 270,
41
+ })
42
+
43
+ expect(getFilledCircleFromKeepoutOutline(fullCircle)).toEqual({
44
+ center: { x: 0, y: 0 },
45
+ radius: 2,
46
+ })
47
+ expect(getFilledCircleFromKeepoutOutline(openPartialArc)).toBeNull()
48
+ expect(getFilledCircleFromKeepoutOutline(closedPartialArc)).toBeNull()
49
+ })
@@ -14,8 +14,8 @@ test("test17 support pcb keepouts", async () => {
14
14
  export const Test17Component = (props: ChipProps) => (
15
15
  <chip
16
16
  footprint={<footprint>
17
- <keepout shape="rect" pcbX="-1.5mm" pcbY="2mm" width="3.2mm" height="1.4mm" />
18
- <keepout shape="circle" pcbX="2.5mm" pcbY="-1mm" radius="0.9mm" />
17
+ <keepout shape="rect" pcbX="-1.5mm" pcbY="2mm" width="3.2mm" height="1.4mm" layers={["top"]} />
18
+ <keepout shape="circle" pcbX="2.5mm" pcbY="-1mm" radius="0.9mm" layers={["top"]} />
19
19
  </footprint>}
20
20
  {...props}
21
21
  />