Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/gerber/any_gerber_command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const gerber_command_map = {
end_of_file,
move_operation,
flash_operation,
// end_region_statement,
end_region_statement,
// flash_operation,
format_specification,
// load_mirroring,
Expand All @@ -62,7 +62,7 @@ export const gerber_command_map = {
select_aperture,
set_unit,
set_layer_polarity,
// start_region_statement,
start_region_statement,
// step_and_repeat,
} as const satisfies Record<string, GerberCommandDef<any, any>>

Expand Down
20 changes: 13 additions & 7 deletions src/gerber/commands/end_region_statement.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { z } from "zod"
import { defineGerberCommand } from "../define-gerber-command"

export const end_region_statement = z
.object({
command_code: z.literal("G37"),
statement: z.string(),
})
.describe("End region statement: Ends the region statement")
export const end_region_statement = defineGerberCommand({
command_code: "G37",
schema: z
.object({
command_code: z.literal("G37").default("G37"),
})
.describe("End region statement: Ends the region statement"),
stringify() {
return "G37*"
},
})

export type EndRegionStatement = z.infer<typeof end_region_statement>
export type EndRegionStatement = z.infer<typeof end_region_statement.schema>
24 changes: 15 additions & 9 deletions src/gerber/commands/start_region_statement.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { z } from "zod"
import { defineGerberCommand } from "../define-gerber-command"

export const start_region_statement = z
.object({
command_code: z.literal("G36"),
statement: z.string(),
})
.describe(
"Start region statement: Starts a region statement which creates a region by defining its contours.",
)
export const start_region_statement = defineGerberCommand({
command_code: "G36",
schema: z
.object({
command_code: z.literal("G36").default("G36"),
})
.describe(
"Start region statement: Starts a region statement which creates a region by defining its contours.",
),
stringify() {
return "G36*"
},
})

export type StartRegionStatement = z.infer<typeof start_region_statement>
export type StartRegionStatement = z.infer<typeof start_region_statement.schema>
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export const defineCommonMacros = (glayer: Array<AnyGerberCommand>) => {
0 $4 = Circle center offset
0 21 = Center Line(Exposure, Width, Height, Center X, Center Y, Rotation)*
0 1 = Circle(Exposure, Diameter, Center X, Center Y, Rotation)*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0-$4,0.0*
1,1,$3,$4,0.0*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0,-$4,0.0*
1,1,$3,0.0,$4,0.0*
`.trim(),
})
.add("define_macro_aperture_template", {
Expand All @@ -31,9 +31,9 @@ export const defineCommonMacros = (glayer: Array<AnyGerberCommand>) => {
0 $3 = Circle diameter (equal to width)*
0 $4 = Circle center offset
0 21 = Center Line(Exposure, Width, Height, Center X, Center Y, Rotation)*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0,0.0-$4*
1,1,$3,0.0,$4*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0,-$4,0.0*
1,1,$3,0.0,$4,0.0*
`.trim(),
})
.add("define_macro_aperture_template", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ export const getApertureConfigFromPcbSmtpad = (
y_size: elm.height,
}
}
if (elm.shape === "polygon") {
// Polygon pads are rendered using region statements. The aperture
// width is arbitrary but must be defined so that a D-code exists when
// the region is drawn. A small round aperture is sufficient.
return {
standard_template_code: "C",
diameter: 0.05,
}
}
throw new Error(`Unsupported shape ${(elm as any).shape}`)
}

Expand Down
37 changes: 37 additions & 0 deletions src/gerber/convert-soup-to-gerber-commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,43 @@ export const convertSoupToGerberCommands = (
gb.add("load_rotation", { rotation_degrees: 0 })
}

glayer.push(...gb.build())
}
}
} else if (element.type === "pcb_smtpad" && element.shape === "polygon") {
if (element.layer === layer) {
for (const glayer of [
glayers[getGerberLayerName(layer, "copper")],
glayers[getGerberLayerName(layer, "soldermask")],
]) {
const apertureConfig = {
standard_template_code: "C",
diameter: 0.05,
}
const apertureNumber = findApertureNumber(glayer, apertureConfig)
const gb = gerberBuilder()
.add("select_aperture", { aperture_number: apertureNumber })
.add("start_region_statement", {})

if (element.points.length > 0) {
gb.add("move_operation", {
x: element.points[0].x,
y: mfy(element.points[0].y),
})
for (let i = 1; i < element.points.length; i++) {
gb.add("plot_operation", {
x: element.points[i].x,
y: mfy(element.points[i].y),
})
}
gb.add("plot_operation", {
x: element.points[0].x,
y: mfy(element.points[0].y),
})
}

gb.add("end_region_statement", {})

glayer.push(...gb.build())
}
}
Expand Down
7 changes: 7 additions & 0 deletions tests/gerber/__snapshots__/polygon-smtpad-bottom.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions tests/gerber/__snapshots__/polygon-smtpad-top.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions tests/gerber/generate-gerber-with-polygon-smtpad.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { test, expect } from "bun:test"
import { convertSoupToGerberCommands } from "src/gerber/convert-soup-to-gerber-commands"
import {
convertSoupToExcellonDrillCommands,
stringifyExcellonDrill,
} from "src/excellon-drill"
import { stringifyGerberCommandLayers } from "src/gerber/stringify-gerber"
import { maybeOutputGerber } from "tests/fixtures/maybe-output-gerber"
import { Circuit } from "@tscircuit/core"

test("Generate gerber with polygon smtpad", async () => {
const circuit = new Circuit()
circuit.add(
<board width={20} height={20}>
<smtpad
shape="polygon"
layer="top"
points={[
{ x: 0, y: 2 },
{ x: -0.588, y: 0.809 },
{ x: -1.902, y: 0.618 },
{ x: -0.951, y: -0.309 },
{ x: -1.176, y: -1.618 },
{ x: 0, y: -1 },
{ x: 1.176, y: -1.618 },
{ x: 0.951, y: -0.309 },
{ x: 1.902, y: 0.618 },
{ x: 0.588, y: 0.809 },
]}
/>
</board>,
)

const circuitJson = circuit.getCircuitJson()

const gerber_cmds = convertSoupToGerberCommands(circuitJson as any)
const excellon_drill_cmds = convertSoupToExcellonDrillCommands({
circuitJson: circuitJson as any,
is_plated: true,
})

const excellonDrillOutput = stringifyExcellonDrill(excellon_drill_cmds)
const gerberOutput = stringifyGerberCommandLayers(gerber_cmds)

await maybeOutputGerber(gerberOutput, excellonDrillOutput)

const sanitizedFCu = gerberOutput.F_Cu.replace(
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g,
"DATE",
)
expect(sanitizedFCu).toMatchInlineSnapshot(`
"%TF.GenerationSoftware,tscircuit,circuit-json-to-gerber,0.0.23*%
%TF.CreationDate,DATE*%
%TF.SameCoordinates,Original*%
%TF.FileFunction,Copper,L1,Top*%
%TF.FilePolarity,Positive*%
%FSLAX46Y46*%
%MOMM*%
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
G04 Created by tscircuit (builder) date DATE*
G01*
G04 APERTURE MACROS START*
%AMHORZPILL*
0 Horizontal pill (stadium) shape macro*
0 Parameters:*
0 $1 = Total width*
0 $2 = Total height*
0 $3 = Circle diameter (equal to height)*
0 $4 = Circle center offset
0 21 = Center Line(Exposure, Width, Height, Center X, Center Y, Rotation)*
0 1 = Circle(Exposure, Diameter, Center X, Center Y, Rotation)*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0,-$4,0.0*
1,1,$3,0.0,$4,0.0*%
%AMVERTPILL*
0 Vertical pill (stadium) shape macro*
0 Parameters:*
0 $1 = Total width*
0 $2 = Total height*
0 $3 = Circle diameter (equal to width)*
0 $4 = Circle center offset
0 21 = Center Line(Exposure, Width, Height, Center X, Center Y, Rotation)*
21,1,$1,$2,0.0,0.0,0.0*
1,1,$3,0.0,-$4,0.0*
1,1,$3,0.0,$4,0.0*%
%AMRoundRect*
0 Rectangle with rounded corners*
0 $1 Corner radius*
0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y Position of each corner*
0 Polygon box body*
4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
0 Circles for rounded corners*
1,1,$1+$1,$2,$3*
1,1,$1+$1,$4,$5*
1,1,$1+$1,$6,$7*
1,1,$1+$1,$8,$9*
0 Rectangles between the rounded corners*
20,1,$1+$1,$2,$3,$4,$5,0*
20,1,$1+$1,$4,$5,$6,$7,0*
20,1,$1+$1,$6,$7,$8,$9,0*
20,1,$1+$1,$8,$9,$2,$3,0*%
G04 APERTURE MACROS END*
G04 aperture START LIST*
%TD*%
G04 aperture END LIST*
M02*"
`)

expect({
...gerberOutput,
"drill.drl": excellonDrillOutput,
}).toMatchGerberSnapshot(import.meta.path, "polygon-smtpad")
})
Loading