Skip to content

Commit db87d17

Browse files
authored
Add corner radius support for PCB cutouts (#66)
* Add corner radius support for PCB cutouts * Adjust circuit-json dependency placement
1 parent 63b6c3e commit db87d17

5 files changed

Lines changed: 194 additions & 52 deletions

File tree

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
"gerber-to-svg": "^4.2.8",
2828
"pcb-stackup": "^4.2.8",
2929
"tsup": "^8.2.4",
30-
"tscircuit": "^0.0.863"
30+
"tscircuit": "^0.0.863",
31+
"circuit-json": "^0.0.317"
3132
},
3233
"peerDependencies": {
3334
"typescript": "^5.0.0",
34-
"tscircuit": "*"
35+
"tscircuit": "*",
36+
"circuit-json": "*"
3537
},
3638
"dependencies": {
3739
"@tscircuit/alphabet": "^0.0.2",

src/gerber/convert-soup-to-gerber-commands/index.ts

Lines changed: 122 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -651,36 +651,36 @@ export const convertSoupToGerberCommands = (
651651
} else {
652652
gerberBuild
653653
.add("move_operation", {
654-
x: center.x - width / 2,
655-
y: mfy(center.y - height / 2),
654+
x: center!.x - width! / 2,
655+
y: mfy(center!.y - height! / 2),
656656
})
657657
.add("plot_operation", {
658-
x: center.x + width / 2,
659-
y: mfy(center.y - height / 2),
658+
x: center!.x + width! / 2,
659+
y: mfy(center!.y - height! / 2),
660660
})
661661
// .add("move_operation", {
662662
// x: center.x + width / 2,
663663
// y: center.y - height / 2,
664664
// })
665665
.add("plot_operation", {
666-
x: center.x + width / 2,
667-
y: mfy(center.y + height / 2),
666+
x: center!.x + width! / 2,
667+
y: mfy(center!.y + height! / 2),
668668
})
669669
// .add("move_operation", {
670670
// x: center.x + width / 2,
671671
// y: center.y + height / 2,
672672
// })
673673
.add("plot_operation", {
674-
x: center.x - width / 2,
675-
y: mfy(center.y + height / 2),
674+
x: center!.x - width! / 2,
675+
y: mfy(center!.y + height! / 2),
676676
})
677677
// .add("move_operation", {
678678
// x: center.x - width / 2,
679679
// y: center.y + height / 2,
680680
// })
681681
.add("plot_operation", {
682-
x: center.x - width / 2,
683-
y: mfy(center.y - height / 2),
682+
x: center!.x - width! / 2,
683+
y: mfy(center!.y - height! / 2),
684684
})
685685
}
686686

@@ -694,24 +694,24 @@ export const convertSoupToGerberCommands = (
694694
aperture_number: 10,
695695
})
696696
.add("move_operation", {
697-
x: center.x - width / 2,
698-
y: mfy(center.y - height / 2),
697+
x: center!.x - width! / 2,
698+
y: mfy(center!.y - height! / 2),
699699
})
700700
.add("plot_operation", {
701-
x: center.x + width / 2,
702-
y: mfy(center.y - height / 2),
701+
x: center!.x + width! / 2,
702+
y: mfy(center!.y - height! / 2),
703703
})
704704
.add("plot_operation", {
705-
x: center.x + width / 2,
706-
y: mfy(center.y + height / 2),
705+
x: center!.x + width! / 2,
706+
y: mfy(center!.y + height! / 2),
707707
})
708708
.add("plot_operation", {
709-
x: center.x - width / 2,
710-
y: mfy(center.y + height / 2),
709+
x: center!.x - width! / 2,
710+
y: mfy(center!.y + height! / 2),
711711
})
712712
.add("plot_operation", {
713-
x: center.x - width / 2,
714-
y: mfy(center.y - height / 2),
713+
x: center!.x - width! / 2,
714+
y: mfy(center!.y - height! / 2),
715715
})
716716

717717
glayer.push(...gerberBuild.build())
@@ -725,45 +725,117 @@ export const convertSoupToGerberCommands = (
725725
const el = element as PcbCutout
726726

727727
if (el.shape === "rect") {
728-
const { center, width, height, rotation } = el
728+
const { center, width, height, rotation, corner_radius } = el
729729
const w = width / 2
730730
const h = height / 2
731+
const r = Math.max(
732+
0,
733+
Math.min(corner_radius ?? 0, Math.abs(w), Math.abs(h)),
734+
)
731735

732-
const points = [
733-
{ x: -w, y: h }, // Top-left
734-
{ x: w, y: h }, // Top-right
735-
{ x: w, y: -h }, // Bottom-right
736-
{ x: -w, y: -h }, // Bottom-left
737-
]
738-
739-
let transformMatrix = identity()
740-
if (rotation) {
741-
const angle_rad = (rotation * Math.PI) / 180
742-
transformMatrix = rotate(angle_rad)
736+
const makeTransformMatrix = () => {
737+
let transformMatrix = identity()
738+
if (rotation) {
739+
const angle_rad = (rotation * Math.PI) / 180
740+
transformMatrix = rotate(angle_rad)
741+
}
742+
return compose(translate(center.x, center.y), transformMatrix)
743743
}
744-
transformMatrix = compose(
745-
translate(center.x, center.y),
746-
transformMatrix,
747-
)
748744

749-
const transformedPoints = points.map((p) =>
750-
applyToPoint(transformMatrix, p),
751-
)
745+
const transformMatrix = makeTransformMatrix()
752746

753-
cutout_builder.add("move_operation", {
754-
x: transformedPoints[0].x,
755-
y: mfy(transformedPoints[0].y),
756-
})
757-
for (let i = 1; i < transformedPoints.length; i++) {
747+
if (r > 0) {
748+
const startPoint = { x: -w + r, y: h }
749+
750+
let currentPoint = applyToPoint(transformMatrix, startPoint)
751+
752+
cutout_builder.add("move_operation", {
753+
x: currentPoint.x,
754+
y: mfy(currentPoint.y),
755+
})
756+
757+
const addLine = (point: { x: number; y: number }) => {
758+
const transformedPoint = applyToPoint(transformMatrix, point)
759+
cutout_builder.add("plot_operation", {
760+
x: transformedPoint.x,
761+
y: mfy(transformedPoint.y),
762+
})
763+
currentPoint = transformedPoint
764+
}
765+
766+
const addArc = (options: {
767+
point: { x: number; y: number }
768+
center: { x: number; y: number }
769+
}) => {
770+
const transformedPoint = applyToPoint(
771+
transformMatrix,
772+
options.point,
773+
)
774+
const transformedCenter = applyToPoint(
775+
transformMatrix,
776+
options.center,
777+
)
778+
779+
cutout_builder
780+
.add("set_movement_mode_to_clockwise_circular", {})
781+
.add("plot_operation", {
782+
x: transformedPoint.x,
783+
y: mfy(transformedPoint.y),
784+
i: transformedCenter.x - currentPoint.x,
785+
j: mfy(transformedCenter.y) - mfy(currentPoint.y),
786+
})
787+
.add("set_movement_mode_to_linear", {})
788+
789+
currentPoint = transformedPoint
790+
}
791+
792+
addLine({ x: w - r, y: h })
793+
addArc({
794+
point: { x: w, y: h - r },
795+
center: { x: w - r, y: h - r },
796+
})
797+
addLine({ x: w, y: -h + r })
798+
addArc({
799+
point: { x: w - r, y: -h },
800+
center: { x: w - r, y: -h + r },
801+
})
802+
addLine({ x: -w + r, y: -h })
803+
addArc({
804+
point: { x: -w, y: -h + r },
805+
center: { x: -w + r, y: -h + r },
806+
})
807+
addLine({ x: -w, y: h - r })
808+
addArc({
809+
point: { x: -w + r, y: h },
810+
center: { x: -w + r, y: h - r },
811+
})
812+
} else {
813+
const points = [
814+
{ x: -w, y: h }, // Top-left
815+
{ x: w, y: h }, // Top-right
816+
{ x: w, y: -h }, // Bottom-right
817+
{ x: -w, y: -h }, // Bottom-left
818+
]
819+
820+
const transformedPoints = points.map((p) =>
821+
applyToPoint(transformMatrix, p),
822+
)
823+
824+
cutout_builder.add("move_operation", {
825+
x: transformedPoints[0].x,
826+
y: mfy(transformedPoints[0].y),
827+
})
828+
for (let i = 1; i < transformedPoints.length; i++) {
829+
cutout_builder.add("plot_operation", {
830+
x: transformedPoints[i].x,
831+
y: mfy(transformedPoints[i].y),
832+
})
833+
}
758834
cutout_builder.add("plot_operation", {
759-
x: transformedPoints[i].x,
760-
y: mfy(transformedPoints[i].y),
835+
x: transformedPoints[0].x,
836+
y: mfy(transformedPoints[0].y),
761837
})
762838
}
763-
cutout_builder.add("plot_operation", {
764-
x: transformedPoints[0].x,
765-
y: mfy(transformedPoints[0].y),
766-
})
767839
} else if (el.shape === "circle") {
768840
const { center, radius } = el
769841

Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from "bun:test"
2+
import type { AnyCircuitElement } from "circuit-json"
3+
import { convertSoupToGerberCommands } from "src/gerber/convert-soup-to-gerber-commands"
4+
import { stringifyGerberCommandLayers } from "src/gerber/stringify-gerber"
5+
6+
const circuitJson: AnyCircuitElement[] = [
7+
{
8+
type: "pcb_board",
9+
pcb_board_id: "pcb_board_0",
10+
center: { x: 0, y: 0 },
11+
width: 30,
12+
height: 30,
13+
material: "fr1",
14+
num_layers: 2,
15+
thickness: 1.2,
16+
},
17+
{
18+
type: "pcb_cutout",
19+
pcb_cutout_id: "pcb_cutout_rect_rounded_0",
20+
shape: "rect",
21+
center: { x: -8, y: 8 },
22+
width: 8,
23+
height: 5,
24+
corner_radius: 2.5,
25+
},
26+
{
27+
type: "pcb_cutout",
28+
pcb_cutout_id: "pcb_cutout_rect_sharp_0",
29+
shape: "rect",
30+
center: { x: 8, y: 8 },
31+
width: 8,
32+
height: 5,
33+
},
34+
{
35+
type: "pcb_cutout",
36+
pcb_cutout_id: "pcb_cutout_rect_rounded_rotated",
37+
shape: "rect",
38+
center: { x: 0, y: -8 },
39+
width: 10,
40+
height: 4,
41+
corner_radius: 0.8,
42+
rotation: 45,
43+
},
44+
]
45+
46+
test("pcb cutout rect with corner radius", () => {
47+
const gerber_cmds = convertSoupToGerberCommands(circuitJson)
48+
const gerberOutput = stringifyGerberCommandLayers(gerber_cmds)
49+
50+
expect(gerberOutput).toMatchGerberSnapshot(
51+
import.meta.path,
52+
"pcb-cutout-corner-radius",
53+
)
54+
})

0 commit comments

Comments
 (0)