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.
- package/dist/{chunk-B56TEDTF.js → chunk-ZIXVXH64.js} +75 -4
- package/dist/cli/main.js +2 -2
- package/dist/lib/index.js +1 -1
- package/lib/generate-footprint-tsx/convert-keepouts.ts +12 -4
- package/lib/generate-footprint-tsx/get-filled-circle-from-keepout-outline.ts +104 -0
- package/package.json +1 -1
- package/tests/get-filled-circle-from-keepout-outline.test.ts +49 -0
- package/tests/test17-support-pcb-keepouts.test.tsx +2 -2
- package/tests/ti-evms/__snapshots__/ti-lm251772evm-pd-roundtrip-pcb-comparison.snap.svg +1 -1
- package/tests/ti-evms/__snapshots__/ti-lm251772evm-pd-roundtrip.test.tsx.snap +4 -0
- package/tests/ti-evms/__snapshots__/ti-lm5155evm-fly-roundtrip-pcb-comparison.snap.svg +1 -1
- package/tests/ti-evms/__snapshots__/ti-lmg342x-bb-evm-roundtrip-pcb-comparison.snap.svg +1 -1
- package/tests/ti-evms/__snapshots__/ti-lmg342x-bb-evm-roundtrip.test.tsx.snap +8 -0
- package/tests/ti-evms/ti-lm251772evm-pd-roundtrip.test.tsx +15 -0
- package/tests/ti-evms/ti-lm5155evm-fly-roundtrip.test.tsx +3 -0
|
@@ -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-
|
|
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.
|
|
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,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
|
@@ -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
|
/>
|