diff --git a/src/gerber/any_gerber_command.ts b/src/gerber/any_gerber_command.ts index 48d604b..953918c 100644 --- a/src/gerber/any_gerber_command.ts +++ b/src/gerber/any_gerber_command.ts @@ -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, @@ -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> diff --git a/src/gerber/commands/end_region_statement.ts b/src/gerber/commands/end_region_statement.ts index e8f85c2..fc32ee5 100644 --- a/src/gerber/commands/end_region_statement.ts +++ b/src/gerber/commands/end_region_statement.ts @@ -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 +export type EndRegionStatement = z.infer diff --git a/src/gerber/commands/start_region_statement.ts b/src/gerber/commands/start_region_statement.ts index 29af2b0..0de2ec6 100644 --- a/src/gerber/commands/start_region_statement.ts +++ b/src/gerber/commands/start_region_statement.ts @@ -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 +export type StartRegionStatement = z.infer diff --git a/src/gerber/convert-soup-to-gerber-commands/define-common-macros.ts b/src/gerber/convert-soup-to-gerber-commands/define-common-macros.ts index e4bdb8c..58388ce 100644 --- a/src/gerber/convert-soup-to-gerber-commands/define-common-macros.ts +++ b/src/gerber/convert-soup-to-gerber-commands/define-common-macros.ts @@ -16,9 +16,9 @@ export const defineCommonMacros = (glayer: Array) => { 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", { @@ -31,9 +31,9 @@ export const defineCommonMacros = (glayer: Array) => { 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", { diff --git a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts index 4f34911..4401c2e 100644 --- a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts +++ b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts @@ -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}`) } diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index e0589ae..476d24e 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -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()) } } diff --git a/tests/gerber/__snapshots__/polygon-smtpad-bottom.snap.svg b/tests/gerber/__snapshots__/polygon-smtpad-bottom.snap.svg new file mode 100644 index 0000000..ba24595 --- /dev/null +++ b/tests/gerber/__snapshots__/polygon-smtpad-bottom.snap.svg @@ -0,0 +1,7 @@ + diff --git a/tests/gerber/__snapshots__/polygon-smtpad-top.snap.svg b/tests/gerber/__snapshots__/polygon-smtpad-top.snap.svg new file mode 100644 index 0000000..ad6c282 --- /dev/null +++ b/tests/gerber/__snapshots__/polygon-smtpad-top.snap.svg @@ -0,0 +1,7 @@ + diff --git a/tests/gerber/generate-gerber-with-polygon-smtpad.test.tsx b/tests/gerber/generate-gerber-with-polygon-smtpad.test.tsx new file mode 100644 index 0000000..01d1590 --- /dev/null +++ b/tests/gerber/generate-gerber-with-polygon-smtpad.test.tsx @@ -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( + + + , + ) + + 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") +})