circuit-json-to-tscircuit 0.0.83 → 0.0.84

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.
@@ -1628,20 +1628,91 @@ var convertHoles = (circuitJson) => {
1628
1628
 
1629
1629
  // lib/generate-footprint-tsx/convert-keepouts.ts
1630
1630
  import { su as su12 } from "@tscircuit/soup-util";
1631
+
1632
+ // lib/generate-footprint-tsx/get-filled-circle-from-keepout-outline.ts
1633
+ var MINIMUM_CIRCLE_VERTEX_COUNT = 8;
1634
+ var CIRCLE_RADIUS_TOLERANCE = 1e-3;
1635
+ var MAXIMUM_ANGULAR_STEP_RATIO = 1.5;
1636
+ var FULL_ROTATION_RADIANS = Math.PI * 2;
1637
+ var getNormalizedAngleDelta = (angleDelta) => {
1638
+ if (angleDelta > Math.PI) return angleDelta - FULL_ROTATION_RADIANS;
1639
+ if (angleDelta <= -Math.PI) return angleDelta + FULL_ROTATION_RADIANS;
1640
+ return angleDelta;
1641
+ };
1642
+ var completesCircularTraversal = ({
1643
+ center,
1644
+ outline,
1645
+ radius
1646
+ }) => {
1647
+ const firstPoint = outline[0];
1648
+ const lastPoint = outline.at(-1);
1649
+ if (!firstPoint || !lastPoint) return false;
1650
+ if (Math.hypot(firstPoint.x - lastPoint.x, firstPoint.y - lastPoint.y) > radius * CIRCLE_RADIUS_TOLERANCE) {
1651
+ return false;
1652
+ }
1653
+ const traversalPoints = outline.slice(0, -1);
1654
+ const maximumAngularStep = FULL_ROTATION_RADIANS / traversalPoints.length * MAXIMUM_ANGULAR_STEP_RATIO;
1655
+ const angleDeltas = traversalPoints.map((point, pointIndex) => {
1656
+ const nextPoint = traversalPoints[(pointIndex + 1) % traversalPoints.length];
1657
+ const pointAngle = Math.atan2(point.y - center.y, point.x - center.x);
1658
+ const nextPointAngle = Math.atan2(
1659
+ nextPoint.y - center.y,
1660
+ nextPoint.x - center.x
1661
+ );
1662
+ return getNormalizedAngleDelta(nextPointAngle - pointAngle);
1663
+ });
1664
+ const traversalDirection = Math.sign(angleDeltas[0] ?? 0);
1665
+ return traversalDirection !== 0 && angleDeltas.every(
1666
+ (angleDelta) => Math.sign(angleDelta) === traversalDirection && Math.abs(angleDelta) <= maximumAngularStep
1667
+ );
1668
+ };
1669
+ var getFilledCircleFromKeepoutOutline = (keepout) => {
1670
+ const outline = keepout.outline;
1671
+ if (outline.length < MINIMUM_CIRCLE_VERTEX_COUNT) return null;
1672
+ const minX = Math.min(...outline.map((point) => point.x));
1673
+ const maxX = Math.max(...outline.map((point) => point.x));
1674
+ const minY = Math.min(...outline.map((point) => point.y));
1675
+ const maxY = Math.max(...outline.map((point) => point.y));
1676
+ const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1677
+ const horizontalRadius = (maxX - minX) / 2;
1678
+ const verticalRadius = (maxY - minY) / 2;
1679
+ const centerlineRadius = (horizontalRadius + verticalRadius) / 2;
1680
+ if (!Number.isFinite(centerlineRadius) || centerlineRadius <= 0) return null;
1681
+ if (Math.abs(horizontalRadius - verticalRadius) > centerlineRadius * CIRCLE_RADIUS_TOLERANCE) {
1682
+ return null;
1683
+ }
1684
+ const isCircular = outline.every((point) => {
1685
+ const pointRadius = Math.hypot(point.x - center.x, point.y - center.y);
1686
+ return Math.abs(pointRadius - centerlineRadius) <= centerlineRadius * CIRCLE_RADIUS_TOLERANCE;
1687
+ });
1688
+ if (!isCircular) return null;
1689
+ if (!completesCircularTraversal({ center, outline, radius: centerlineRadius }))
1690
+ return null;
1691
+ const strokeRadius = keepout.stroke_width / 2;
1692
+ if (strokeRadius < centerlineRadius) return null;
1693
+ return { center, radius: centerlineRadius + strokeRadius };
1694
+ };
1695
+
1696
+ // lib/generate-footprint-tsx/convert-keepouts.ts
1697
+ var formatLayersAttribute = (layers) => ` layers={${JSON.stringify(layers)}}`;
1631
1698
  var convertKeepouts = (circuitJson) => {
1632
1699
  const pcbKeepouts = su12(circuitJson).pcb_keepout.list();
1633
1700
  const elementStrings = [];
1634
1701
  for (const keepout of pcbKeepouts) {
1635
1702
  if (keepout.shape === "rect") {
1636
1703
  elementStrings.push(
1637
- `<keepout shape="rect" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" width="${formatMm(keepout.width)}" height="${formatMm(keepout.height)}" />`
1704
+ `<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
1705
  );
1639
1706
  } else if (keepout.shape === "circle") {
1640
1707
  elementStrings.push(
1641
- `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}" />`
1708
+ `<keepout shape="circle" pcbX="${formatMm(keepout.center.x)}" pcbY="${formatMm(keepout.center.y)}" radius="${formatMm(keepout.radius)}"${formatLayersAttribute(keepout.layers)} />`
1709
+ );
1710
+ } else if (keepout.shape === "outline") {
1711
+ const circle = getFilledCircleFromKeepoutOutline(keepout);
1712
+ if (!circle) continue;
1713
+ elementStrings.push(
1714
+ `<keepout shape="circle" pcbX="${formatMm(circle.center.x)}" pcbY="${formatMm(circle.center.y)}" radius="${formatMm(circle.radius)}"${formatLayersAttribute(keepout.layers)} />`
1642
1715
  );
1643
- } else {
1644
- console.warn(`Unhandled pcb_keepout shape: ${keepout.shape}`);
1645
1716
  }
1646
1717
  }
1647
1718
  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-ZIXVXH64.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.83",
20
20
  repository: {
21
21
  type: "git",
22
22
  url: "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
package/dist/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertCircuitJsonToTscircuit
3
- } from "../chunk-B56TEDTF.js";
3
+ } from "../chunk-ZIXVXH64.js";
4
4
  export {
5
5
  convertCircuitJsonToTscircuit
6
6
  };
@@ -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.84",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
@@ -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
  />