From bd2a86626366856cb30c4d49dcec274755fee678 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sun, 30 Nov 2025 09:36:40 -0800 Subject: [PATCH 1/2] Fix board outline closure for edge cuts --- .../convert-soup-to-gerber-commands/index.ts | 19 +++++++- .../generate-board-outline-closure.test.tsx | 44 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/gerber/generate-board-outline-closure.test.tsx diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index 7e96fef..96c763c 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -644,9 +644,24 @@ export const convertSoupToGerberCommands = ( aperture_number: 10, }) if (outline && outline.length > 2) { - gerberBuild.add("move_operation", outline[0]) + const firstPoint = outline[0] + gerberBuild.add("move_operation", { + x: firstPoint.x, + y: mfy(firstPoint.y), + }) for (let i = 1; i < outline.length; i++) { - gerberBuild.add("plot_operation", outline[i]) + gerberBuild.add("plot_operation", { + x: outline[i].x, + y: mfy(outline[i].y), + }) + } + + const lastPoint = outline[outline.length - 1] + if (lastPoint.x !== firstPoint.x || lastPoint.y !== firstPoint.y) { + gerberBuild.add("plot_operation", { + x: firstPoint.x, + y: mfy(firstPoint.y), + }) } } else { gerberBuild diff --git a/tests/gerber/generate-board-outline-closure.test.tsx b/tests/gerber/generate-board-outline-closure.test.tsx new file mode 100644 index 0000000..f38f5ac --- /dev/null +++ b/tests/gerber/generate-board-outline-closure.test.tsx @@ -0,0 +1,44 @@ +import { Circuit } from "@tscircuit/core" +import { expect, test } from "bun:test" +import { convertSoupToGerberCommands } from "src/gerber/convert-soup-to-gerber-commands" + +// Regression test: outlines should be closed in the Edge_Cuts layer +// even if the provided outline path does not repeat the starting point. +test("board outline paths are closed and flipped when required", () => { + const circuit = new Circuit() + circuit.add( + , // outline intentionally does not repeat the starting point + ) + + const soup = circuit.getCircuitJson() + const edgeCuts = convertSoupToGerberCommands(soup as any, { + flip_y_axis: true, + }).Edge_Cuts + + const outlineCommands = edgeCuts.filter( + (cmd) => cmd.command_code === "D02" || cmd.command_code === "D01", + ) + + expect( + outlineCommands.map(({ command_code, x, y }) => ({ + command_code, + x, + y: Object.is(y, -0) ? 0 : y, + })), + ).toEqual([ + { command_code: "D02", x: 0, y: 0 }, + { command_code: "D01", x: 10, y: 0 }, + { command_code: "D01", x: 10, y: -5 }, + { command_code: "D01", x: 0, y: -5 }, + { command_code: "D01", x: 0, y: 0 }, + ]) +}) From 2d63d8cedcb3dd838da704637cbe9f16d12852ac Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sun, 30 Nov 2025 10:01:16 -0800 Subject: [PATCH 2/2] Add SVG snapshot for board outline closure test --- .../__snapshots__/outline-closure-bottom.snap.svg | 7 +++++++ .../__snapshots__/outline-closure-top.snap.svg | 7 +++++++ tests/gerber/generate-board-outline-closure.test.tsx | 12 +++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/gerber/__snapshots__/outline-closure-bottom.snap.svg create mode 100644 tests/gerber/__snapshots__/outline-closure-top.snap.svg diff --git a/tests/gerber/__snapshots__/outline-closure-bottom.snap.svg b/tests/gerber/__snapshots__/outline-closure-bottom.snap.svg new file mode 100644 index 0000000..3ddef7b --- /dev/null +++ b/tests/gerber/__snapshots__/outline-closure-bottom.snap.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/outline-closure-top.snap.svg b/tests/gerber/__snapshots__/outline-closure-top.snap.svg new file mode 100644 index 0000000..3e242f5 --- /dev/null +++ b/tests/gerber/__snapshots__/outline-closure-top.snap.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/tests/gerber/generate-board-outline-closure.test.tsx b/tests/gerber/generate-board-outline-closure.test.tsx index f38f5ac..dd879fc 100644 --- a/tests/gerber/generate-board-outline-closure.test.tsx +++ b/tests/gerber/generate-board-outline-closure.test.tsx @@ -1,10 +1,11 @@ import { Circuit } from "@tscircuit/core" import { expect, test } from "bun:test" import { convertSoupToGerberCommands } from "src/gerber/convert-soup-to-gerber-commands" +import { stringifyGerberCommandLayers } from "src/gerber/stringify-gerber" // Regression test: outlines should be closed in the Edge_Cuts layer // even if the provided outline path does not repeat the starting point. -test("board outline paths are closed and flipped when required", () => { +test("board outline paths are closed and flipped when required", async () => { const circuit = new Circuit() circuit.add( { { command_code: "D01", x: 0, y: -5 }, { command_code: "D01", x: 0, y: 0 }, ]) + + const gerberOutput = stringifyGerberCommandLayers( + convertSoupToGerberCommands(soup as any, { flip_y_axis: true }), + ) + + await expect(gerberOutput).toMatchGerberSnapshot( + import.meta.path, + "outline-closure", + ) })