From 890ca5135cb7083fa4ca608b8f5c76ce2965c1be Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 09:38:44 -0700 Subject: [PATCH 01/17] wip --- .gitignore | 3 +- docs/CIRCUIT_JSON_PCB_OVERVIEW.md | 340 ++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 docs/CIRCUIT_JSON_PCB_OVERVIEW.md diff --git a/.gitignore b/.gitignore index ad4640b..ea9213d 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,5 @@ dist yalc.lock gerber-output -gerber-output.zip \ No newline at end of file +gerber-output.zip +.aider* diff --git a/docs/CIRCUIT_JSON_PCB_OVERVIEW.md b/docs/CIRCUIT_JSON_PCB_OVERVIEW.md new file mode 100644 index 0000000..f21406d --- /dev/null +++ b/docs/CIRCUIT_JSON_PCB_OVERVIEW.md @@ -0,0 +1,340 @@ +# Circuit JSON Specification: PCB Component Overview + +> Created at 2024-10-23T22:17:25.274Z +> Latest Version: https://github.com/tscircuit/circuit-json/blob/main/docs/PCB_COMPONENT_OVERVIEW.md + +Any type below can be imported from `circuit-json`. Every type has a corresponding +snake_case version which is a zod type that can be used to parse unknown json, +for example `PcbComponent` has a `pcb_component.parse` function that you +can also import. + +```ts +export interface PcbFabricationNotePath { + type: "pcb_fabrication_note_path" + pcb_fabrication_note_path_id: string + pcb_component_id: string + layer: LayerRef + route: Point[] + stroke_width: Length + color?: string +} + +export interface PcbComponent { + type: "pcb_component" + pcb_component_id: string + source_component_id: string + center: Point + layer: LayerRef + rotation: Rotation + width: Length + height: Length +} + +export interface PcbPortNotMatchedError { + type: "pcb_port_not_matched_error" + pcb_error_id: string + message: string + pcb_component_ids: string[] +} + +export interface PcbSolderPasteCircle { + type: "pcb_solder_paste" + shape: "circle" + pcb_solder_paste_id: string + x: Distance + y: Distance + radius: number + layer: LayerRef + pcb_component_id?: string + pcb_smtpad_id?: string +} + +export interface PcbSolderPasteRect { + type: "pcb_solder_paste" + shape: "rect" + pcb_solder_paste_id: string + x: Distance + y: Distance + width: number + height: number + layer: LayerRef + pcb_component_id?: string + pcb_smtpad_id?: string +} + +export type PcbSolderPaste = PcbSolderPasteCircle | PcbSolderPasteRect + +export interface PcbSilkscreenText { + type: "pcb_silkscreen_text" + pcb_silkscreen_text_id: string + font: "tscircuit2024" + font_size: Length + pcb_component_id: string + text: string + layer: LayerRef + is_mirrored?: boolean + anchor_position: Point + anchor_alignment: + | "center" + | "top_left" + | "top_right" + | "bottom_left" + | "bottom_right" +} + +export interface PcbTraceError { + type: "pcb_trace_error" + pcb_trace_error_id: string + error_type: "pcb_trace_error" + message: string + center?: Point + pcb_trace_id: string + source_trace_id: string + pcb_component_ids: string[] + pcb_port_ids: string[] +} + +export interface PcbSilkscreenPill { + type: "pcb_silkscreen_pill" + pcb_silkscreen_pill_id: string + pcb_component_id: string + center: Point + width: Length + height: Length + layer: LayerRef +} + +export interface PcbPlatedHoleCircle { + type: "pcb_plated_hole" + shape: "circle" + outer_diameter: number + hole_diameter: number + x: Distance + y: Distance + layers: LayerRef[] + port_hints?: string[] + pcb_component_id?: string + pcb_port_id?: string + pcb_plated_hole_id: string +} + +export interface PcbPlatedHoleOval { + type: "pcb_plated_hole" + shape: "oval" | "pill" + outer_width: number + outer_height: number + hole_width: number + hole_height: number + x: Distance + y: Distance + layers: LayerRef[] + port_hints?: string[] + pcb_component_id?: string + pcb_port_id?: string + pcb_plated_hole_id: string +} + +export type PcbPlatedHole = PcbPlatedHoleCircle | PcbPlatedHoleOval + +export interface PcbFabricationNoteText { + type: "pcb_fabrication_note_text" + pcb_fabrication_note_text_id: string + font: "tscircuit2024" + font_size: Length + pcb_component_id: string + text: string + layer: VisibleLayer + anchor_position: Point + anchor_alignment: + | "center" + | "top_left" + | "top_right" + | "bottom_left" + | "bottom_right" + color?: string +} + +export interface PcbSilkscreenCircle { + type: "pcb_silkscreen_circle" + pcb_silkscreen_circle_id: string + pcb_component_id: string + center: Point + radius: Length + layer: VisibleLayer +} + +export interface PcbSilkscreenPath { + type: "pcb_silkscreen_path" + pcb_silkscreen_path_id: string + pcb_component_id: string + layer: VisibleLayerRef + route: Point[] + stroke_width: Length +} + +export interface PcbText { + type: "pcb_text" + pcb_text_id: string + text: string + center: Point + layer: LayerRef + width: Length + height: Length + lines: number + align: "bottom-left" +} + +export interface PCBKeepout { + type: "pcb_keepout" + shape: "rect" | "circle" + center: Point + width?: Distance + height?: Distance + radius?: Distance + pcb_keepout_id: string + layers: string[] + description?: string +} + +export interface PcbVia { + type: "pcb_via" + pcb_via_id: string + x: Distance + y: Distance + outer_diameter: Distance + hole_diameter: Distance + layers: LayerRef[] + pcb_trace_id?: string +} + +export interface PcbSilkscreenOval { + type: "pcb_silkscreen_oval" + pcb_silkscreen_oval_id: string + pcb_component_id: string + center: Point + radius_x: Distance + radius_y: Distance + layer: VisibleLayer +} + +export interface PcbPlacementError { + type: "pcb_placement_error" + pcb_placement_error_id: string + message: string +} + +export interface PcbPort { + type: "pcb_port" + pcb_port_id: string + source_port_id: string + pcb_component_id: string + x: Distance + y: Distance + layers: LayerRef[] +} + +export interface PcbSmtPadCircle { + type: "pcb_smtpad" + shape: "circle" + pcb_smtpad_id: string + x: Distance + y: Distance + radius: number + layer: LayerRef + port_hints?: string[] + pcb_component_id?: string + pcb_port_id?: string +} + +export interface PcbSmtPadRect { + type: "pcb_smtpad" + shape: "rect" + pcb_smtpad_id: string + x: Distance + y: Distance + width: number + height: number + layer: LayerRef + port_hints?: string[] + pcb_component_id?: string + pcb_port_id?: string +} + +export type PcbSmtPad = PcbSmtPadCircle | PcbSmtPadRect + +export interface PcbSilkscreenLine { + type: "pcb_silkscreen_line" + pcb_silkscreen_line_id: string + pcb_component_id: string + stroke_width: Distance + x1: Distance + y1: Distance + x2: Distance + y2: Distance + layer: VisibleLayer +} + +export interface PcbHoleCircleOrSquare { + type: "pcb_hole" + pcb_hole_id: string + hole_shape: "circle" | "square" + hole_diameter: number + x: Distance + y: Distance +} + +export interface PcbHoleOval { + type: "pcb_hole" + pcb_hole_id: string + hole_shape: "oval" + hole_width: number + hole_height: number + x: Distance + y: Distance +} + +export type PcbHole = PcbHoleCircleOrSquare | PcbHoleOval + +export interface PcbTraceRoutePointWire { + route_type: "wire" + x: Distance + y: Distance + width: Distance + start_pcb_port_id?: string + end_pcb_port_id?: string + layer: LayerRef +} + +export interface PcbTraceRoutePointVia { + route_type: "via" + x: Distance + y: Distance + from_layer: string + to_layer: string +} + +export type PcbTraceRoutePoint = PcbTraceRoutePointWire | PcbTraceRoutePointVia + +export interface PcbTrace { + type: "pcb_trace" + source_trace_id?: string + pcb_component_id?: string + pcb_trace_id: string + route_order_index?: number + route_thickness_mode?: "constant" | "interpolated" + should_round_corners?: boolean + route: Array +} + +export interface PcbBoard { + type: "pcb_board" + pcb_board_id: string + width: Length + height: Length + thickness: Length + num_layers: number + center: Point + outline?: Point[] +} + +``` \ No newline at end of file From af8a5a345b022967497c72bb684e7e6ba6942400 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 09:57:48 -0700 Subject: [PATCH 02/17] support for custom macro names --- bun.lockb | Bin 118430 -> 126966 bytes package.json | 9 ++- .../commands/define_aperture_template.ts | 69 +++++++++++++----- .../define-common-macros.ts | 14 ++++ .../defineAperturesForLayer.ts | 34 +++++++-- .../generate-gerber-with-pill-shape.test.tsx | 40 ++++++++++ 6 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 tests/gerber/generate-gerber-with-pill-shape.test.tsx diff --git a/bun.lockb b/bun.lockb index ddf3d8f167b48f147b8a755e623f312e02f87a63..c3a36213a08677afda03bba91f761ec7bf81051b 100755 GIT binary patch delta 25802 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 aYrbdZ&n%0H)z_JYZ0)t@z)cos_rCzI%C)cn diff --git a/package.json b/package.json index c4539b4..c132e3b 100644 --- a/package.json +++ b/package.json @@ -13,20 +13,21 @@ ], "devDependencies": { "@biomejs/biome": "^1.8.3", - "@tscircuit/core": "^0.0.102", + "@tscircuit/core": "^0.0.131", "@types/bun": "^1.1.8", "@types/node": "^22.5.2", - "@types/react": "^18.3.11", + "@types/react": "^18.3.12", "bun-match-svg": "^0.0.3", "gerber-to-svg": "^4.2.8", "pcb-stackup": "^4.2.8", + "react": "^18.3.1", "tsup": "^8.2.4" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "circuit-json": "^0.0.91" }, "dependencies": { - "circuit-json": "^0.0.83", "fast-json-stable-stringify": "^2.1.0" } } diff --git a/src/gerber/commands/define_aperture_template.ts b/src/gerber/commands/define_aperture_template.ts index 3e95cda..78ccf01 100644 --- a/src/gerber/commands/define_aperture_template.ts +++ b/src/gerber/commands/define_aperture_template.ts @@ -30,11 +30,31 @@ const polygon_template = z.object({ hole_diameter: z.number().optional(), }) -const aperture_template_config = z.discriminatedUnion( +const standard_aperture_template_config = z.discriminatedUnion( "standard_template_code", [circle_template, rectangle_template, obround_template, polygon_template], ) +const pill_template = z.object({ + macro_name: z.literal("PILL"), + x_size: z.number(), + y_size: z.number(), +}) + +const roundrect_template = z.object({ + macro_name: z.literal("ROUNDRECT"), +}) + +const macro_aperture_template_config = z.discriminatedUnion("macro_name", [ + pill_template, + roundrect_template, +]) + +const aperture_template_config = z.union([ + standard_aperture_template_config, + macro_aperture_template_config, +]) + export const define_aperture_template = defineGerberCommand({ command_code: "ADD", schema: aperture_template_config.and( @@ -44,29 +64,38 @@ export const define_aperture_template = defineGerberCommand({ }), ), stringify(props) { - const { aperture_number, standard_template_code } = props - let commandString = `%ADD${aperture_number}${standard_template_code},` - - if (standard_template_code === "C") { - commandString += `${props.diameter.toFixed(6)}` - } else if ( - standard_template_code === "R" || - standard_template_code === "O" - ) { - commandString += `${props.x_size.toFixed(6)}X${props.y_size.toFixed(6)}` - } else if (standard_template_code === "P") { - commandString += `${props.outer_diameter}X${props.number_of_vertices}X${ - props.rotation ? `X${props.rotation}` : "" - }` + if ("macro_name" in props) { + // TODO + return "" } + if ("standard_template_code" in props) { + const { aperture_number, standard_template_code } = props + let commandString = `%ADD${aperture_number}${standard_template_code},` - if (props.hole_diameter) { - commandString += `X${props.hole_diameter.toFixed(6)}` - } + if (standard_template_code === "C") { + commandString += `${props.diameter.toFixed(6)}` + } else if ( + standard_template_code === "R" || + standard_template_code === "O" + ) { + commandString += `${props.x_size.toFixed(6)}X${props.y_size.toFixed(6)}` + } else if (standard_template_code === "P") { + commandString += `${props.outer_diameter}X${props.number_of_vertices}X${ + props.rotation ? `X${props.rotation}` : "" + }` + } - commandString += "*%" + if (props.hole_diameter) { + commandString += `X${props.hole_diameter.toFixed(6)}` + } - return commandString + commandString += "*%" + + return commandString + } + throw new Error( + `Invalid aperture template config: ${JSON.stringify(props)}`, + ) }, }) 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 37a1f0b..efe270a 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 @@ -5,6 +5,20 @@ export const defineCommonMacros = (glayer: Array) => { glayer.push( ...gerberBuilder() .add("comment", { comment: "APERTURE MACROS START" }) + .add("define_macro_aperture_template", { + macro_name: "PILL", + template_code: ` +0 Pill shape (rounded rectangle with semicircle ends)* +0 $1 = width* +0 $2 = height* +0 Center rectangle* +21,1,$1,0,0,$2-$1,0* +0 Left circle* +1,1,$1,0,-($2-$1)/2,0* +0 Right circle* +1,1,$1,0,($2-$1)/2,0*% +`.trim(), + }) .add("define_macro_aperture_template", { macro_name: "RoundRect", template_code: ` diff --git a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts index a366de8..92e003d 100644 --- a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts +++ b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts @@ -135,18 +135,36 @@ export const getApertureConfigFromPcbSolderPaste = ( throw new Error(`Unsupported shape ${(elm as any).shape}`) } -export const getApertureConfigFromCirclePcbPlatedHole = ( +export const getApertureConfigFromPcbPlatedHole = ( elm: PCBPlatedHole, ): ApertureTemplateConfig => { - if (!("outer_diameter" in elm && "hole_diameter" in elm)) { - throw new Error( - `Invalid shape called in getApertureConfigFromCirclePcbPlatedHole: ${elm.shape}`, - ) + if (elm.shape === "circle") { + if (!("outer_diameter" in elm && "hole_diameter" in elm)) { + throw new Error( + "Invalid circle shape in getApertureConfigFromPcbPlatedHole: missing diameters", + ) + } + return { + standard_template_code: "C", + diameter: elm.outer_diameter, + } } - return { - standard_template_code: "C", - diameter: elm.outer_diameter, + if (elm.shape === "pill") { + if (!("outer_width" in elm && "outer_height" in elm)) { + throw new Error( + "Invalid pill shape in getApertureConfigFromPcbPlatedHole: missing dimensions", + ) + } + // For pill shapes, we'll use a custom macro that combines rectangles and circles + return { + macro_name: "PILL", + x_size: elm.outer_width, + y_size: elm.outer_height, + } } + throw new Error( + `Unsupported shape in getApertureConfigFromPcbPlatedHole: ${elm.shape}`, + ) } export const getApertureConfigFromCirclePcbHole = ( diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx new file mode 100644 index 0000000..e204266 --- /dev/null +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -0,0 +1,40 @@ +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 pill shape", async () => { + const circuit = new Circuit() + circuit.add( + + + , + ) + + const circuitJson = circuit.getCircuitJson() + const gerber_cmds = convertSoupToGerberCommands(circuitJson) + const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ + soup: circuitJson, + is_plated: true, + }) + + const excellonDrillOutput = stringifyExcellonDrill(excellon_drill_cmds) + const gerberOutput = stringifyGerberCommandLayers(gerber_cmds) + + await maybeOutputGerber(gerberOutput, excellonDrillOutput) + + expect(gerberOutput).toMatchGerberSnapshot(import.meta.path, "pill-shape") +}) From 90193cc866e17716bb51f7c78d888d76086dc55b Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 10:09:29 -0700 Subject: [PATCH 03/17] pill shape --- .../defineAperturesForLayer.ts | 4 ++-- src/gerber/convert-soup-to-gerber-commands/index.ts | 4 ++-- tests/gerber/__snapshots__/pill-shape-bottom.snap.svg | 7 +++++++ tests/gerber/__snapshots__/pill-shape-top.snap.svg | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 tests/gerber/__snapshots__/pill-shape-bottom.snap.svg create mode 100644 tests/gerber/__snapshots__/pill-shape-top.snap.svg diff --git a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts index 92e003d..322328b 100644 --- a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts +++ b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts @@ -222,11 +222,11 @@ function getAllApertureTemplateConfigsForLayer( } else if (elm.type === "pcb_plated_hole") { if (elm.layers.includes(layer)) { if (elm.shape === "circle") { - addConfigIfNew(getApertureConfigFromCirclePcbPlatedHole(elm)) + addConfigIfNew(getApertureConfigFromPcbPlatedHole(elm)) } else if (elm.shape === "oval") { console.warn("NOT IMPLEMENTED: drawing gerber for oval plated hole") } else if (elm.shape === "pill") { - console.warn("NOT IMPLEMENTED: drawing gerber for oval plated pill") + addConfigIfNew(getApertureConfigFromPcbPlatedHole(elm)) } } } else if (elm.type === "pcb_hole") { diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index 70cee99..6a6a164 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -5,7 +5,7 @@ import type { LayerToGerberCommandsMap } from "./GerberLayerName" import { defineAperturesForLayer, getApertureConfigFromCirclePcbHole, - getApertureConfigFromCirclePcbPlatedHole, + getApertureConfigFromPcbPlatedHole, getApertureConfigFromPcbSilkscreenPath, getApertureConfigFromPcbSmtpad, getApertureConfigFromPcbSolderPaste, @@ -199,7 +199,7 @@ export const convertSoupToGerberCommands = ( .add("select_aperture", { aperture_number: findApertureNumber( glayer, - getApertureConfigFromCirclePcbPlatedHole(element), + getApertureConfigFromPcbPlatedHole(element), ), }) .add("flash_operation", { x: element.x, y: mfy(element.y) }) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg new file mode 100644 index 0000000..4eb8da9 --- /dev/null +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg new file mode 100644 index 0000000..7b38d67 --- /dev/null +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -0,0 +1,7 @@ + \ No newline at end of file From 2a404de3d88cd54679730fe0961f10fbe3ad2f2d Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 10:20:44 -0700 Subject: [PATCH 04/17] wip PILL impl --- src/gerber/commands/define_aperture_template.ts | 15 +++++++++++++-- .../getCommandHeaders.ts | 2 +- .../generate-gerber-with-pill-shape.test.tsx | 2 ++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/gerber/commands/define_aperture_template.ts b/src/gerber/commands/define_aperture_template.ts index 78ccf01..4395d10 100644 --- a/src/gerber/commands/define_aperture_template.ts +++ b/src/gerber/commands/define_aperture_template.ts @@ -65,8 +65,19 @@ export const define_aperture_template = defineGerberCommand({ ), stringify(props) { if ("macro_name" in props) { - // TODO - return "" + const { aperture_number, macro_name } = props + let commandString = `%ADD${aperture_number}${macro_name},` + + if (macro_name === "PILL") { + // For PILL macro, we need width (x_size) and height (y_size) + commandString += `${props.x_size.toFixed(6)}X${props.y_size.toFixed(6)}` + } else if (macro_name === "ROUNDRECT") { + // Handle ROUNDRECT if needed + throw new Error("ROUNDRECT macro not implemented yet") + } + + commandString += "*%" + return commandString } if ("standard_template_code" in props) { const { aperture_number, standard_template_code } = props diff --git a/src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts b/src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts index 9145a54..bdea7e6 100644 --- a/src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts +++ b/src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts @@ -51,7 +51,7 @@ export const getCommandHeaders = (opts: { gerberBuilder() .add("add_attribute_on_file", { attribute_name: "GenerationSoftware", - attribute_value: `tscircuit,builder,${packageJson.version}`, + attribute_value: `tscircuit,circuit-json-to-gerber,${packageJson.version}`, }) .add("add_attribute_on_file", { attribute_name: "CreationDate", diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index e204266..dce1b3d 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -34,6 +34,8 @@ test("Generate gerber with pill shape", async () => { const excellonDrillOutput = stringifyExcellonDrill(excellon_drill_cmds) const gerberOutput = stringifyGerberCommandLayers(gerber_cmds) + console.log(gerberOutput.F_Cu) + await maybeOutputGerber(gerberOutput, excellonDrillOutput) expect(gerberOutput).toMatchGerberSnapshot(import.meta.path, "pill-shape") From c0e9046dd7f57e33370b8bc3cab065f0d4f9282f Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 10:26:18 -0700 Subject: [PATCH 05/17] fix macros not being defined --- src/gerber/convert-soup-to-gerber-commands/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index 6a6a164..61ffbef 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -2,6 +2,7 @@ import type { AnyCircuitElement } from "circuit-json" import { pairs } from "../utils/pairs" import { gerberBuilder } from "../gerber-builder" import type { LayerToGerberCommandsMap } from "./GerberLayerName" +import { defineCommonMacros } from "./define-common-macros" import { defineAperturesForLayer, getApertureConfigFromCirclePcbHole, @@ -72,7 +73,7 @@ export const convertSoupToGerberCommands = ( "B_SilkScreen", ] as const) { const glayer = glayers[glayer_name] - // defineCommonMacros(glayer) + defineCommonMacros(glayer) defineAperturesForLayer({ soup, glayer, From a584732fd1cc19f7e9d223d3e140d99cade2adba Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 10:33:28 -0700 Subject: [PATCH 06/17] drawing of pill as circle (incorrect) --- .../defineAperturesForLayer.ts | 9 +-------- .../findApertureNumber.ts | 2 +- .../convert-soup-to-gerber-commands/index.ts | 12 ++---------- .../__snapshots__/pill-shape-bottom.diff.png | Bin 0 -> 211 bytes .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- .../gerber/__snapshots__/pill-shape-top.diff.png | Bin 0 -> 213 bytes .../gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- .../generate-gerber-with-pill-shape.test.tsx | 4 ++-- 8 files changed, 20 insertions(+), 35 deletions(-) create mode 100644 tests/gerber/__snapshots__/pill-shape-bottom.diff.png create mode 100644 tests/gerber/__snapshots__/pill-shape-top.diff.png diff --git a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts index 322328b..9117618 100644 --- a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts +++ b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts @@ -155,7 +155,6 @@ export const getApertureConfigFromPcbPlatedHole = ( "Invalid pill shape in getApertureConfigFromPcbPlatedHole: missing dimensions", ) } - // For pill shapes, we'll use a custom macro that combines rectangles and circles return { macro_name: "PILL", x_size: elm.outer_width, @@ -221,13 +220,7 @@ function getAllApertureTemplateConfigsForLayer( } } else if (elm.type === "pcb_plated_hole") { if (elm.layers.includes(layer)) { - if (elm.shape === "circle") { - addConfigIfNew(getApertureConfigFromPcbPlatedHole(elm)) - } else if (elm.shape === "oval") { - console.warn("NOT IMPLEMENTED: drawing gerber for oval plated hole") - } else if (elm.shape === "pill") { - addConfigIfNew(getApertureConfigFromPcbPlatedHole(elm)) - } + addConfigIfNew(getApertureConfigFromPcbPlatedHole(elm)) } } else if (elm.type === "pcb_hole") { if (elm.hole_shape === "circle") diff --git a/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts b/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts index 653e957..a15f210 100644 --- a/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts +++ b/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts @@ -21,7 +21,7 @@ export const findApertureNumber = ( command.standard_template_code === "C" && command.diameter === trace_width, ) - } else if ("standard_template_code" in search_params) { + } else if ("standard_template_code" in search_params || "macro_name" in search_params) { aperture = glayer.find( (command): command is DefineAperatureTemplateCommand => command.command_code === "ADD" && diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index 61ffbef..ca58d44 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -189,19 +189,11 @@ export const convertSoupToGerberCommands = ( glayers[getGerberLayerName(layer, "copper")], glayers[getGerberLayerName(layer, "soldermask")], ]) { - if (element.shape !== "circle") { - console.warn( - "NOT IMPLEMENTED: drawing gerber for non-circle plated hole", - ) - continue - } + const apertureConfig = getApertureConfigFromPcbPlatedHole(element) glayer.push( ...gerberBuilder() .add("select_aperture", { - aperture_number: findApertureNumber( - glayer, - getApertureConfigFromPcbPlatedHole(element), - ), + aperture_number: findApertureNumber(glayer, apertureConfig), }) .add("flash_operation", { x: element.x, y: mfy(element.y) }) .build(), diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.diff.png b/tests/gerber/__snapshots__/pill-shape-bottom.diff.png new file mode 100644 index 0000000000000000000000000000000000000000..90f85bd68e8ef8f0f36b60ed597ddd3c7cae9a3d GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^mLSZ*1SFZOL@og-&H|6fVj%AY2s3W24$1}!wtBia zhE&XXJL4qp0Rx`H7HhxXZ}^+mxW)8<0y9U`+QVmhc03Xedt5pz{Fd&1G3CoTkFVBr zmvwLIe!2Q-czD$tnfKQto;U1ZSUvq?CG+e1m8BQ={CcOp=GbeK^;@FrblsLk9$WoW z?5$X;_~r1^>z{4BmZ*^)*uQm6?slHn(Pifsojdz)_A};9e*~M1C%NwhI*7s3)z4*} HQ$iB}Di2#m literal 0 HcmV?d00001 diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 4eb8da9..c7a344d 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.diff.png b/tests/gerber/__snapshots__/pill-shape-top.diff.png new file mode 100644 index 0000000000000000000000000000000000000000..6e1f5d07c24350ab4519c71d6050922a1026a2c2 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^mLSZ*1SFZOL@og-&H|6fVj%AY2s3W24$1}!wtKob zhE&XXJHwFgfB_FvrSAX#2TRXh$eSwUE?#+}V*>AuOmB%mLG4dbzfI4H?LNw3%3bMt zziYGTX6{$o&%)n*xuKWO5O(jMdeT$o@0}6W9oP9@h5ysu=oXi~|4~lqZi$6yitArQ z7j8=`bF4l3r%SHe+UHDUVgJ!Jz1w;2Z2vG%wEW$f{h4;VrG@<1f~S=Moy6ej>gTe~ HDWM4fwS!W? literal 0 HcmV?d00001 diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 7b38d67..b289e56 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index dce1b3d..92a2c84 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -14,9 +14,9 @@ test("Generate gerber with pill shape", async () => { Date: Fri, 25 Oct 2024 11:07:01 -0700 Subject: [PATCH 07/17] horz pill working --- .../commands/define_aperture_template.ts | 31 +++++++++++++--- .../define-common-macros.ts | 37 ++++++++++++++----- .../defineAperturesForLayer.ts | 14 ++++++- .../findApertureNumber.ts | 6 ++- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++---- .../__snapshots__/pill-shape-top.snap.svg | 14 +++---- .../generate-gerber-with-pill-shape.test.tsx | 13 ++++++- 7 files changed, 95 insertions(+), 34 deletions(-) diff --git a/src/gerber/commands/define_aperture_template.ts b/src/gerber/commands/define_aperture_template.ts index 4395d10..5ebc64e 100644 --- a/src/gerber/commands/define_aperture_template.ts +++ b/src/gerber/commands/define_aperture_template.ts @@ -35,18 +35,38 @@ const standard_aperture_template_config = z.discriminatedUnion( [circle_template, rectangle_template, obround_template, polygon_template], ) -const pill_template = z.object({ - macro_name: z.literal("PILL"), +const horz_pill_template = z.object({ + macro_name: z.literal("HORZPILL"), x_size: z.number(), y_size: z.number(), + circle_diameter: z.number(), + circle_center_offset: z.number(), +}) + +const vert_pill_template = z.object({ + macro_name: z.literal("VERTPILL"), + x_size: z.number(), + y_size: z.number(), + circle_diameter: z.number(), + circle_center_offset: z.number(), }) const roundrect_template = z.object({ macro_name: z.literal("ROUNDRECT"), + corner_radius: z.number(), + corner_1_x: z.number(), + corner_1_y: z.number(), + corner_2_x: z.number(), + corner_2_y: z.number(), + corner_3_x: z.number(), + corner_3_y: z.number(), + corner_4_x: z.number(), + corner_4_y: z.number(), }) const macro_aperture_template_config = z.discriminatedUnion("macro_name", [ - pill_template, + horz_pill_template, + vert_pill_template, roundrect_template, ]) @@ -68,9 +88,8 @@ export const define_aperture_template = defineGerberCommand({ const { aperture_number, macro_name } = props let commandString = `%ADD${aperture_number}${macro_name},` - if (macro_name === "PILL") { - // For PILL macro, we need width (x_size) and height (y_size) - commandString += `${props.x_size.toFixed(6)}X${props.y_size.toFixed(6)}` + if (macro_name === "HORZPILL" || macro_name === "VERTPILL") { + commandString += `${props.x_size.toFixed(6)}X${props.y_size.toFixed(6)}X${props.circle_diameter.toFixed(6)}X${props.circle_center_offset.toFixed(6)}` } else if (macro_name === "ROUNDRECT") { // Handle ROUNDRECT if needed throw new Error("ROUNDRECT macro not implemented yet") 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 efe270a..2673388 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 @@ -6,17 +6,34 @@ export const defineCommonMacros = (glayer: Array) => { ...gerberBuilder() .add("comment", { comment: "APERTURE MACROS START" }) .add("define_macro_aperture_template", { - macro_name: "PILL", + macro_name: "HORZPILL", template_code: ` -0 Pill shape (rounded rectangle with semicircle ends)* -0 $1 = width* -0 $2 = height* -0 Center rectangle* -21,1,$1,0,0,$2-$1,0* -0 Left circle* -1,1,$1,0,-($2-$1)/2,0* -0 Right circle* -1,1,$1,0,($2-$1)/2,0*% +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 ((width-diameter)/2)* +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*% +`.trim(), + }) + .add("define_macro_aperture_template", { + macro_name: "VERTPILL", + template_code: ` +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 ((height-diameter)/2)* +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*% `.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 9117618..bacd6ba 100644 --- a/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts +++ b/src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts @@ -155,10 +155,22 @@ export const getApertureConfigFromPcbPlatedHole = ( "Invalid pill shape in getApertureConfigFromPcbPlatedHole: missing dimensions", ) } + + if (elm.outer_width > elm.outer_height) { + return { + macro_name: "HORZPILL", + x_size: elm.outer_width, + y_size: elm.outer_height, + circle_diameter: Math.min(elm.outer_width, elm.outer_height), + circle_center_offset: elm.outer_width / 2, + } + } return { - macro_name: "PILL", + macro_name: "VERTPILL", x_size: elm.outer_width, y_size: elm.outer_height, + circle_diameter: Math.min(elm.outer_width, elm.outer_height), + circle_center_offset: elm.outer_height / 2, } } throw new Error( diff --git a/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts b/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts index a15f210..15644d9 100644 --- a/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts +++ b/src/gerber/convert-soup-to-gerber-commands/findApertureNumber.ts @@ -17,11 +17,15 @@ export const findApertureNumber = ( const trace_width = search_params.trace_width aperture = glayer.find( (command): command is DefineAperatureTemplateCommand => + "standard_template_code" in command && command.command_code === "ADD" && command.standard_template_code === "C" && command.diameter === trace_width, ) - } else if ("standard_template_code" in search_params || "macro_name" in search_params) { + } else if ( + "standard_template_code" in search_params || + "macro_name" in search_params + ) { aperture = glayer.find( (command): command is DefineAperatureTemplateCommand => command.command_code === "ADD" && diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index c7a344d..e852a2f 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index b289e56..ec7b44a 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index 92a2c84..0bb7e2d 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -15,12 +15,21 @@ test("Generate gerber with pill shape", async () => { + , ) From 394369ec9911e53eca72c642f06fb071fc96d9ab Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:11:37 -0700 Subject: [PATCH 08/17] wip --- .../define-common-macros.ts | 4 ++-- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- tests/gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) 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 2673388..5f557ed 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 @@ -13,7 +13,7 @@ export const defineCommonMacros = (glayer: Array) => { 0 $1 = Total width* 0 $2 = Total height* 0 $3 = Circle diameter (equal to height)* -0 $4 = Circle center offset ((width-diameter)/2)* +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* @@ -29,7 +29,7 @@ export const defineCommonMacros = (glayer: Array) => { 0 $1 = Total width* 0 $2 = Total height* 0 $3 = Circle diameter (equal to width)* -0 $4 = Circle center offset ((height-diameter)/2)* +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* diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index e852a2f..92075d2 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index ec7b44a..1d82eb6 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file From 688b262a85c355420b31251387605f82992e6002 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:18:03 -0700 Subject: [PATCH 09/17] vertical pill drawing --- .../define-common-macros.ts | 6 +++--- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- tests/gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- .../generate-gerber-with-pill-shape.test.tsx | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) 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 5f557ed..e4bdb8c 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 @@ -18,7 +18,7 @@ export const defineCommonMacros = (glayer: Array) => { 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*% +1,1,$3,$4,0.0* `.trim(), }) .add("define_macro_aperture_template", { @@ -33,7 +33,7 @@ export const defineCommonMacros = (glayer: Array) => { 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*% +1,1,$3,0.0,$4* `.trim(), }) .add("define_macro_aperture_template", { @@ -53,7 +53,7 @@ export const defineCommonMacros = (glayer: Array) => { 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*% +20,1,$1+$1,$8,$9,$2,$3,0* `.trim(), }) .add("comment", { comment: "APERTURE MACROS END" }) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 92075d2..8edd21a 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 1d82eb6..3157c4c 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index 0bb7e2d..3ef65a5 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -24,10 +24,10 @@ test("Generate gerber with pill shape", async () => { , From f555ef4fbd78c76ae7d6da2da4f1ea4c5934eb66 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:22:01 -0700 Subject: [PATCH 10/17] drill in snapshot output for pill --- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- tests/gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- .../generate-gerber-with-pill-shape.test.tsx | 7 +++++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 8edd21a..639f739 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 3157c4c..4940dfc 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index 3ef65a5..af405cf 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -43,9 +43,12 @@ test("Generate gerber with pill shape", async () => { const excellonDrillOutput = stringifyExcellonDrill(excellon_drill_cmds) const gerberOutput = stringifyGerberCommandLayers(gerber_cmds) - console.log(gerberOutput.F_Cu) + console.log(excellonDrillOutput) await maybeOutputGerber(gerberOutput, excellonDrillOutput) - expect(gerberOutput).toMatchGerberSnapshot(import.meta.path, "pill-shape") + expect({ + ...gerberOutput, + "drill.drl": excellonDrillOutput, + }).toMatchGerberSnapshot(import.meta.path, "pill-shape") }) From 81fdbc95f02f1a3aefe82665adc6991178b17b2b Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:31:16 -0700 Subject: [PATCH 11/17] refactor "soup" to "circuitJson" --- .../convert-soup-to-excellon-drill-commands.ts | 8 ++++---- .../convert-soup-to-excellon-drill.test.ts | 2 +- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- tests/gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- .../gerber/generate-board-outline-gerber.test.tsx | 2 +- tests/gerber/generate-gerber-macrokeypad.test.tsx | 2 +- .../generate-gerber-with-basic-elements.test.tsx | 2 +- .../generate-gerber-with-pill-shape.test.tsx | 5 ++++- 8 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts index adf6bb2..c1a84e6 100644 --- a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts +++ b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts @@ -3,11 +3,11 @@ import type { AnyExcellonDrillCommand } from "./any-excellon-drill-command-map" import { excellonDrill } from "./excellon-drill-builder" export const convertSoupToExcellonDrillCommands = ({ - soup, + circuitJson, is_plated, flip_y_axis = false, }: { - soup: Array + circuitJson: Array is_plated: boolean flip_y_axis?: boolean }): Array => { @@ -45,7 +45,7 @@ export const convertSoupToExcellonDrillCommands = ({ const diameterToToolNumber: Record = {} // Define tools - for (const element of soup) { + for (const element of circuitJson) { if ( element.type === "pcb_plated_hole" || element.type === "pcb_hole" || @@ -73,7 +73,7 @@ export const convertSoupToExcellonDrillCommands = ({ // Execute drills for tool N for (let i = 10; i < tool_counter; i++) { builder.add("use_tool", { tool_number: i }) - for (const element of soup) { + for (const element of circuitJson) { if ( element.type === "pcb_plated_hole" || element.type === "pcb_hole" || diff --git a/tests/excellon-drill/convert-soup-to-excellon-drill.test.ts b/tests/excellon-drill/convert-soup-to-excellon-drill.test.ts index 6c8072a..892333e 100644 --- a/tests/excellon-drill/convert-soup-to-excellon-drill.test.ts +++ b/tests/excellon-drill/convert-soup-to-excellon-drill.test.ts @@ -54,7 +54,7 @@ test("generate excellon drill text from axial resistor", async () => { ] const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ - soup: soup, + circuitJson: soup, is_plated: true, }) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 639f739..7e81711 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 4940dfc..7d0bd2e 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-board-outline-gerber.test.tsx b/tests/gerber/generate-board-outline-gerber.test.tsx index 5427754..0ea56aa 100644 --- a/tests/gerber/generate-board-outline-gerber.test.tsx +++ b/tests/gerber/generate-board-outline-gerber.test.tsx @@ -65,7 +65,7 @@ test("Generate simple board with a multi-layer trace", async () => { const gerber_cmds = convertSoupToGerberCommands(soup) const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ - soup: soup, + circuitJson: soup, is_plated: true, }) const edgecut_gerber = stringifyGerberCommands(gerber_cmds.Edge_Cuts) diff --git a/tests/gerber/generate-gerber-macrokeypad.test.tsx b/tests/gerber/generate-gerber-macrokeypad.test.tsx index f6bbb3d..4f68ae1 100644 --- a/tests/gerber/generate-gerber-macrokeypad.test.tsx +++ b/tests/gerber/generate-gerber-macrokeypad.test.tsx @@ -18,7 +18,7 @@ test("Generate gerber of macrokeypad", async () => { const circuitJson = circuit.getCircuitJson() const gerber_cmds = convertSoupToGerberCommands(circuitJson) const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ - soup: circuitJson, + circuitJson: circuitJson, is_plated: true, }) const edgecut_gerber = stringifyGerberCommands(gerber_cmds.Edge_Cuts) diff --git a/tests/gerber/generate-gerber-with-basic-elements.test.tsx b/tests/gerber/generate-gerber-with-basic-elements.test.tsx index 52ccb0d..33abf96 100644 --- a/tests/gerber/generate-gerber-with-basic-elements.test.tsx +++ b/tests/gerber/generate-gerber-with-basic-elements.test.tsx @@ -33,7 +33,7 @@ test("Generate simple gerber with basic elements", async () => { const soup = circuit.getCircuitJson() const gerber_cmds = convertSoupToGerberCommands(soup) const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ - soup: soup, + circuitJson: soup, is_plated: true, }) const edgecut_gerber = stringifyGerberCommands(gerber_cmds.Edge_Cuts) diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index af405cf..bb80d19 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -34,9 +34,12 @@ test("Generate gerber with pill shape", async () => { ) const circuitJson = circuit.getCircuitJson() + + console.log(circuitJson) + const gerber_cmds = convertSoupToGerberCommands(circuitJson) const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ - soup: circuitJson, + circuitJson: circuitJson, is_plated: true, }) From 095bc04fcc806f67022a4e764f65ae45befdfe33 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:32:36 -0700 Subject: [PATCH 12/17] add unplated and plated handling --- .../__snapshots__/pill-shape-bottom.snap.svg | 14 +++++++------- tests/gerber/__snapshots__/pill-shape-top.snap.svg | 14 +++++++------- .../generate-gerber-with-pill-shape.test.tsx | 12 +++++++++++- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 7e81711..700730c 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 7d0bd2e..8b90510 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index bb80d19..7e0bdf9 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -42,16 +42,26 @@ test("Generate gerber with pill shape", async () => { circuitJson: circuitJson, is_plated: true, }) + const excellon_drill_cmds_unplated = convertSoupToExcellonDrillCommands({ + circuitJson: circuitJson, + is_plated: false, + }) const excellonDrillOutput = stringifyExcellonDrill(excellon_drill_cmds) + const excellonDrillOutputUnplated = stringifyExcellonDrill( + excellon_drill_cmds_unplated, + ) const gerberOutput = stringifyGerberCommandLayers(gerber_cmds) - console.log(excellonDrillOutput) + console.log("plated\n", excellonDrillOutput) + + console.log("unplated\n", excellonDrillOutputUnplated) await maybeOutputGerber(gerberOutput, excellonDrillOutput) expect({ ...gerberOutput, "drill.drl": excellonDrillOutput, + "drill_npth.drl": excellonDrillOutputUnplated, }).toMatchGerberSnapshot(import.meta.path, "pill-shape") }) From cfaf70c5f24c188429e8d7ae7f0d71f8e931b731 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:35:40 -0700 Subject: [PATCH 13/17] pill drill initial --- ...convert-soup-to-excellon-drill-commands.ts | 30 +++++++++++++++---- .../__snapshots__/pill-shape-bottom.snap.svg | 14 ++++----- .../__snapshots__/pill-shape-top.snap.svg | 14 ++++----- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts index c1a84e6..fbfb582 100644 --- a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts +++ b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts @@ -51,16 +51,26 @@ export const convertSoupToExcellonDrillCommands = ({ element.type === "pcb_hole" || element.type === "pcb_via" ) { - if (!("hole_diameter" in element)) continue - if (!diameterToToolNumber[element.hole_diameter]) { + let hole_diameter: number | undefined + + if ("hole_diameter" in element) { + hole_diameter = element.hole_diameter + } else if (element.type === "pcb_plated_hole" && element.shape === "pill") { + // For pill shapes, use the minimum dimension as the hole diameter + hole_diameter = Math.min(element.hole_width, element.hole_height) + } + + if (!hole_diameter) continue + + if (!diameterToToolNumber[hole_diameter]) { builder.add("aper_function_header", { is_plated: true, }) builder.add("define_tool", { tool_number: tool_counter, - diameter: element.hole_diameter, + diameter: hole_diameter, }) - diameterToToolNumber[element.hole_diameter] = tool_counter + diameterToToolNumber[hole_diameter] = tool_counter tool_counter++ } } @@ -84,8 +94,16 @@ export const convertSoupToExcellonDrillCommands = ({ (element.type === "pcb_plated_hole" || element.type === "pcb_via") ) continue - if (!("hole_diameter" in element)) continue - if (diameterToToolNumber[element.hole_diameter] === i) { + let hole_diameter: number | undefined + + if ("hole_diameter" in element) { + hole_diameter = element.hole_diameter + } else if (element.type === "pcb_plated_hole" && element.shape === "pill") { + hole_diameter = Math.min(element.hole_width, element.hole_height) + } + + if (!hole_diameter) continue + if (diameterToToolNumber[hole_diameter] === i) { builder.add("drill_at", { x: element.x, y: element.y * (flip_y_axis ? -1 : 1), diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index 700730c..b375e55 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 8b90510..66ece7d 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file From 124595fd29442828554c6479b9a8efbbdf902ac0 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 11:58:25 -0700 Subject: [PATCH 14/17] use G85 for hole drilling --- README.md | 1 + .../any-excellon-drill-command-map.ts | 2 + src/excellon-drill/commands/G01.ts | 10 +++ src/excellon-drill/commands/G85.ts | 10 +++ src/excellon-drill/commands/index.ts | 2 + ...convert-soup-to-excellon-drill-commands.ts | 39 +++++++++-- .../convert-soup-to-gerber-commands/index.ts | 66 +++++++++++++++++-- .../__snapshots__/pill-shape-bottom.snap.svg | 14 ++-- .../__snapshots__/pill-shape-top.snap.svg | 14 ++-- 9 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 src/excellon-drill/commands/G01.ts create mode 100644 src/excellon-drill/commands/G85.ts diff --git a/README.md b/README.md index 611050a..a4b3277 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,4 @@ Some components of this gerber system: ## References - [Gerber Format Specification (2022)](https://www.ucamco.com/files/downloads/file_en/456/gerber-layer-format-specification-revision-2022-02_en.pdf?7b3ca7f0753aa2d77f5f9afe31b9f826) +- [Excellon Drill Format Specification](https://gist.github.com/katyo/5692b935abc085b1037e) diff --git a/src/excellon-drill/any-excellon-drill-command-map.ts b/src/excellon-drill/any-excellon-drill-command-map.ts index 2216a73..c79bbab 100644 --- a/src/excellon-drill/any-excellon-drill-command-map.ts +++ b/src/excellon-drill/any-excellon-drill-command-map.ts @@ -3,6 +3,8 @@ import type { ExcellonDrillCommandDef } from "./define-excellon-drill-command" import * as EDCMD from "./commands" export const excellon_drill_command_map = { + G01: EDCMD.G01, + G85: EDCMD.G85, M48: EDCMD.M48, M95: EDCMD.M95, FMAT: EDCMD.FMAT, diff --git a/src/excellon-drill/commands/G01.ts b/src/excellon-drill/commands/G01.ts new file mode 100644 index 0000000..8c40f51 --- /dev/null +++ b/src/excellon-drill/commands/G01.ts @@ -0,0 +1,10 @@ +import { z } from "zod" +import { defineExcellonDrillCommand } from "../define-excellon-drill-command" + +export const G01 = defineExcellonDrillCommand({ + command_code: "G01", + schema: z.object({ + command_code: z.literal("G01").default("G01"), + }), + stringify: () => "G01", +}) diff --git a/src/excellon-drill/commands/G85.ts b/src/excellon-drill/commands/G85.ts new file mode 100644 index 0000000..3dda6d1 --- /dev/null +++ b/src/excellon-drill/commands/G85.ts @@ -0,0 +1,10 @@ +import { z } from "zod" +import { defineExcellonDrillCommand } from "../define-excellon-drill-command" + +export const G85 = defineExcellonDrillCommand({ + command_code: "G85", + schema: z.object({ + command_code: z.literal("G85").default("G85"), + }), + stringify: () => "G85", +}) diff --git a/src/excellon-drill/commands/index.ts b/src/excellon-drill/commands/index.ts index 5cd4d94..73783d0 100644 --- a/src/excellon-drill/commands/index.ts +++ b/src/excellon-drill/commands/index.ts @@ -1,5 +1,7 @@ export * from "./FMAT" +export * from "./G01" export * from "./G05" +export * from "./G85" export * from "./G90" export * from "./M30" export * from "./M48" diff --git a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts index fbfb582..a01deb2 100644 --- a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts +++ b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts @@ -100,10 +100,41 @@ export const convertSoupToExcellonDrillCommands = ({ hole_diameter = element.hole_diameter } else if (element.type === "pcb_plated_hole" && element.shape === "pill") { hole_diameter = Math.min(element.hole_width, element.hole_height) - } - - if (!hole_diameter) continue - if (diameterToToolNumber[hole_diameter] === i) { + + // For pill shapes, we need to route the hole + if (diameterToToolNumber[hole_diameter] === i) { + const y_multiplier = flip_y_axis ? -1 : 1 + + if (element.hole_width > element.hole_height) { + // Horizontal pill + const offset = (element.hole_width - element.hole_height) / 2 + builder + .add("G85", {}) + .add("drill_at", { + x: element.x - offset, + y: element.y * y_multiplier, + }) + .add("drill_at", { + x: element.x + offset, + y: element.y * y_multiplier, + }) + } else { + // Vertical pill + const offset = (element.hole_height - element.hole_width) / 2 + builder + .add("G85", {}) + .add("drill_at", { + x: element.x, + y: (element.y - offset) * y_multiplier, + }) + .add("drill_at", { + x: element.x, + y: (element.y + offset) * y_multiplier, + }) + } + } + } else if (!hole_diameter) continue + else if (diameterToToolNumber[hole_diameter] === i) { builder.add("drill_at", { x: element.x, y: element.y * (flip_y_axis ? -1 : 1), diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index ca58d44..2656d57 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -189,15 +189,67 @@ export const convertSoupToGerberCommands = ( glayers[getGerberLayerName(layer, "copper")], glayers[getGerberLayerName(layer, "soldermask")], ]) { - const apertureConfig = getApertureConfigFromPcbPlatedHole(element) - glayer.push( - ...gerberBuilder() + if (element.shape === "pill") { + // For pill shapes, use a circular aperture and draw the connecting line + const circleApertureConfig = { + standard_template_code: "C" as const, + diameter: element.outer_width > element.outer_height ? + element.outer_height : element.outer_width + } + + // Find existing aperture number or get next available + let aperture_number = 10 + try { + aperture_number = findApertureNumber(glayer, circleApertureConfig) + } catch { + aperture_number = Math.max(...glayer + .filter(cmd => 'aperture_number' in cmd) + .map(cmd => (cmd as any).aperture_number), 9) + 1 + + // Add the aperture definition + glayer.push( + ...gerberBuilder() + .add("define_aperture_template", { + aperture_number, + ...circleApertureConfig + }) + .build() + ) + } + + const gb = gerberBuilder() .add("select_aperture", { - aperture_number: findApertureNumber(glayer, apertureConfig), + aperture_number, }) - .add("flash_operation", { x: element.x, y: mfy(element.y) }) - .build(), - ) + + if (element.outer_width > element.outer_height) { + // Horizontal pill + const offset = (element.outer_width - element.outer_height) / 2 + gb.add("flash_operation", { x: element.x - offset, y: mfy(element.y) }) + .add("move_operation", { x: element.x - offset, y: mfy(element.y) }) + .add("plot_operation", { x: element.x + offset, y: mfy(element.y) }) + .add("flash_operation", { x: element.x + offset, y: mfy(element.y) }) + } else { + // Vertical pill + const offset = (element.outer_height - element.outer_width) / 2 + gb.add("flash_operation", { x: element.x, y: mfy(element.y - offset) }) + .add("move_operation", { x: element.x, y: mfy(element.y - offset) }) + .add("plot_operation", { x: element.x, y: mfy(element.y + offset) }) + .add("flash_operation", { x: element.x, y: mfy(element.y + offset) }) + } + + glayer.push(...gb.build()) + } else { + const apertureConfig = getApertureConfigFromPcbPlatedHole(element) + glayer.push( + ...gerberBuilder() + .add("select_aperture", { + aperture_number: findApertureNumber(glayer, apertureConfig), + }) + .add("flash_operation", { x: element.x, y: mfy(element.y) }) + .build(), + ) + } } } } else if (element.type === "pcb_hole") { diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index b375e55..f8ca33b 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index 66ece7d..b96a4d5 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file From c1ce7cb3ad4b00df5f7d37b2f80b86ce0d8e84a9 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 12:14:16 -0700 Subject: [PATCH 15/17] upgrade snapshots, fix issue where regular holes weren't being drawn --- .gitignore | 1 + ...convert-soup-to-excellon-drill-commands.ts | 19 +++++++++++------- tests/fixtures/preload.ts | 1 - .../__snapshots__/pill-shape-bottom.diff.png | Bin 211 -> 0 bytes .../__snapshots__/pill-shape-bottom.snap.svg | 14 ++++++------- .../__snapshots__/pill-shape-top.diff.png | Bin 213 -> 0 bytes .../__snapshots__/pill-shape-top.snap.svg | 14 ++++++------- .../__snapshots__/simple1-bottom.snap.svg | 14 ++++++------- .../gerber/__snapshots__/simple1-top.snap.svg | 14 ++++++------- .../__snapshots__/simple2-bottom.snap.svg | 14 ++++++------- .../gerber/__snapshots__/simple2-top.snap.svg | 14 ++++++------- .../__snapshots__/simple3-bottom.snap.svg | 14 ++++++------- .../gerber/__snapshots__/simple3-top.snap.svg | 14 ++++++------- .../generate-gerber-with-pill-shape.test.tsx | 6 ------ 14 files changed, 69 insertions(+), 70 deletions(-) delete mode 100644 tests/gerber/__snapshots__/pill-shape-bottom.diff.png delete mode 100644 tests/gerber/__snapshots__/pill-shape-top.diff.png diff --git a/.gitignore b/.gitignore index ea9213d..b151154 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,4 @@ yalc.lock gerber-output gerber-output.zip .aider* +*.diff.png diff --git a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts index a01deb2..f6fa024 100644 --- a/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts +++ b/src/excellon-drill/convert-soup-to-excellon-drill-commands.ts @@ -52,16 +52,19 @@ export const convertSoupToExcellonDrillCommands = ({ element.type === "pcb_via" ) { let hole_diameter: number | undefined - + if ("hole_diameter" in element) { hole_diameter = element.hole_diameter - } else if (element.type === "pcb_plated_hole" && element.shape === "pill") { + } else if ( + element.type === "pcb_plated_hole" && + element.shape === "pill" + ) { // For pill shapes, use the minimum dimension as the hole diameter hole_diameter = Math.min(element.hole_width, element.hole_height) } if (!hole_diameter) continue - + if (!diameterToToolNumber[hole_diameter]) { builder.add("aper_function_header", { is_plated: true, @@ -95,16 +98,18 @@ export const convertSoupToExcellonDrillCommands = ({ ) continue let hole_diameter: number | undefined - + if ("hole_diameter" in element) { hole_diameter = element.hole_diameter - } else if (element.type === "pcb_plated_hole" && element.shape === "pill") { + } + + if (element.type === "pcb_plated_hole" && element.shape === "pill") { hole_diameter = Math.min(element.hole_width, element.hole_height) - + // For pill shapes, we need to route the hole if (diameterToToolNumber[hole_diameter] === i) { const y_multiplier = flip_y_axis ? -1 : 1 - + if (element.hole_width > element.hole_height) { // Horizontal pill const offset = (element.hole_width - element.hole_height) / 2 diff --git a/tests/fixtures/preload.ts b/tests/fixtures/preload.ts index 62360a9..7462d72 100644 --- a/tests/fixtures/preload.ts +++ b/tests/fixtures/preload.ts @@ -36,7 +36,6 @@ async function toMatchGerberSnapshot( } expect.extend({ - // biome-ignore lint/suspicious/noExplicitAny: toMatchGerberSnapshot: toMatchGerberSnapshot as any, }) diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.diff.png b/tests/gerber/__snapshots__/pill-shape-bottom.diff.png deleted file mode 100644 index 90f85bd68e8ef8f0f36b60ed597ddd3c7cae9a3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^mLSZ*1SFZOL@og-&H|6fVj%AY2s3W24$1}!wtBia zhE&XXJL4qp0Rx`H7HhxXZ}^+mxW)8<0y9U`+QVmhc03Xedt5pz{Fd&1G3CoTkFVBr zmvwLIe!2Q-czD$tnfKQto;U1ZSUvq?CG+e1m8BQ={CcOp=GbeK^;@FrblsLk9$WoW z?5$X;_~r1^>z{4BmZ*^)*uQm6?slHn(Pifsojdz)_A};9e*~M1C%NwhI*7s3)z4*} HQ$iB}Di2#m diff --git a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg index f8ca33b..35500f5 100644 --- a/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/pill-shape-top.diff.png b/tests/gerber/__snapshots__/pill-shape-top.diff.png deleted file mode 100644 index 6e1f5d07c24350ab4519c71d6050922a1026a2c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^mLSZ*1SFZOL@og-&H|6fVj%AY2s3W24$1}!wtKob zhE&XXJHwFgfB_FvrSAX#2TRXh$eSwUE?#+}V*>AuOmB%mLG4dbzfI4H?LNw3%3bMt zziYGTX6{$o&%)n*xuKWO5O(jMdeT$o@0}6W9oP9@h5ysu=oXi~|4~lqZi$6yitArQ z7j8=`bF4l3r%SHe+UHDUVgJ!Jz1w;2Z2vG%wEW$f{h4;VrG@<1f~S=Moy6ej>gTe~ HDWM4fwS!W? diff --git a/tests/gerber/__snapshots__/pill-shape-top.snap.svg b/tests/gerber/__snapshots__/pill-shape-top.snap.svg index b96a4d5..57db981 100644 --- a/tests/gerber/__snapshots__/pill-shape-top.snap.svg +++ b/tests/gerber/__snapshots__/pill-shape-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple1-bottom.snap.svg b/tests/gerber/__snapshots__/simple1-bottom.snap.svg index adaafc7..79fff85 100644 --- a/tests/gerber/__snapshots__/simple1-bottom.snap.svg +++ b/tests/gerber/__snapshots__/simple1-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple1-top.snap.svg b/tests/gerber/__snapshots__/simple1-top.snap.svg index a1dc021..a2e3d2f 100644 --- a/tests/gerber/__snapshots__/simple1-top.snap.svg +++ b/tests/gerber/__snapshots__/simple1-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple2-bottom.snap.svg b/tests/gerber/__snapshots__/simple2-bottom.snap.svg index b9ac0b7..270adb0 100644 --- a/tests/gerber/__snapshots__/simple2-bottom.snap.svg +++ b/tests/gerber/__snapshots__/simple2-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple2-top.snap.svg b/tests/gerber/__snapshots__/simple2-top.snap.svg index 4eb3054..bd72f9b 100644 --- a/tests/gerber/__snapshots__/simple2-top.snap.svg +++ b/tests/gerber/__snapshots__/simple2-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple3-bottom.snap.svg b/tests/gerber/__snapshots__/simple3-bottom.snap.svg index ede6fe8..1823391 100644 --- a/tests/gerber/__snapshots__/simple3-bottom.snap.svg +++ b/tests/gerber/__snapshots__/simple3-bottom.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/__snapshots__/simple3-top.snap.svg b/tests/gerber/__snapshots__/simple3-top.snap.svg index dd5ee0a..6117409 100644 --- a/tests/gerber/__snapshots__/simple3-top.snap.svg +++ b/tests/gerber/__snapshots__/simple3-top.snap.svg @@ -1,7 +1,7 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/tests/gerber/generate-gerber-with-pill-shape.test.tsx b/tests/gerber/generate-gerber-with-pill-shape.test.tsx index 7e0bdf9..981cc00 100644 --- a/tests/gerber/generate-gerber-with-pill-shape.test.tsx +++ b/tests/gerber/generate-gerber-with-pill-shape.test.tsx @@ -35,8 +35,6 @@ test("Generate gerber with pill shape", async () => { const circuitJson = circuit.getCircuitJson() - console.log(circuitJson) - const gerber_cmds = convertSoupToGerberCommands(circuitJson) const excellon_drill_cmds = convertSoupToExcellonDrillCommands({ circuitJson: circuitJson, @@ -53,10 +51,6 @@ test("Generate gerber with pill shape", async () => { ) const gerberOutput = stringifyGerberCommandLayers(gerber_cmds) - console.log("plated\n", excellonDrillOutput) - - console.log("unplated\n", excellonDrillOutputUnplated) - await maybeOutputGerber(gerberOutput, excellonDrillOutput) expect({ From 2553f73a030663e3c1bf8a334029e3683f8bbb43 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 12:15:18 -0700 Subject: [PATCH 16/17] format --- .../convert-soup-to-gerber-commands/index.ts | 78 +++++++++++++------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/src/gerber/convert-soup-to-gerber-commands/index.ts b/src/gerber/convert-soup-to-gerber-commands/index.ts index 2656d57..9562ed6 100644 --- a/src/gerber/convert-soup-to-gerber-commands/index.ts +++ b/src/gerber/convert-soup-to-gerber-commands/index.ts @@ -193,51 +193,83 @@ export const convertSoupToGerberCommands = ( // For pill shapes, use a circular aperture and draw the connecting line const circleApertureConfig = { standard_template_code: "C" as const, - diameter: element.outer_width > element.outer_height ? - element.outer_height : element.outer_width + diameter: + element.outer_width > element.outer_height + ? element.outer_height + : element.outer_width, } // Find existing aperture number or get next available let aperture_number = 10 try { - aperture_number = findApertureNumber(glayer, circleApertureConfig) + aperture_number = findApertureNumber( + glayer, + circleApertureConfig, + ) } catch { - aperture_number = Math.max(...glayer - .filter(cmd => 'aperture_number' in cmd) - .map(cmd => (cmd as any).aperture_number), 9) + 1 - + aperture_number = + Math.max( + ...glayer + .filter((cmd) => "aperture_number" in cmd) + .map((cmd) => (cmd as any).aperture_number), + 9, + ) + 1 + // Add the aperture definition glayer.push( ...gerberBuilder() .add("define_aperture_template", { aperture_number, - ...circleApertureConfig + ...circleApertureConfig, }) - .build() + .build(), ) } - - const gb = gerberBuilder() - .add("select_aperture", { - aperture_number, - }) + + const gb = gerberBuilder().add("select_aperture", { + aperture_number, + }) if (element.outer_width > element.outer_height) { // Horizontal pill const offset = (element.outer_width - element.outer_height) / 2 - gb.add("flash_operation", { x: element.x - offset, y: mfy(element.y) }) - .add("move_operation", { x: element.x - offset, y: mfy(element.y) }) - .add("plot_operation", { x: element.x + offset, y: mfy(element.y) }) - .add("flash_operation", { x: element.x + offset, y: mfy(element.y) }) + gb.add("flash_operation", { + x: element.x - offset, + y: mfy(element.y), + }) + .add("move_operation", { + x: element.x - offset, + y: mfy(element.y), + }) + .add("plot_operation", { + x: element.x + offset, + y: mfy(element.y), + }) + .add("flash_operation", { + x: element.x + offset, + y: mfy(element.y), + }) } else { // Vertical pill const offset = (element.outer_height - element.outer_width) / 2 - gb.add("flash_operation", { x: element.x, y: mfy(element.y - offset) }) - .add("move_operation", { x: element.x, y: mfy(element.y - offset) }) - .add("plot_operation", { x: element.x, y: mfy(element.y + offset) }) - .add("flash_operation", { x: element.x, y: mfy(element.y + offset) }) + gb.add("flash_operation", { + x: element.x, + y: mfy(element.y - offset), + }) + .add("move_operation", { + x: element.x, + y: mfy(element.y - offset), + }) + .add("plot_operation", { + x: element.x, + y: mfy(element.y + offset), + }) + .add("flash_operation", { + x: element.x, + y: mfy(element.y + offset), + }) } - + glayer.push(...gb.build()) } else { const apertureConfig = getApertureConfigFromPcbPlatedHole(element) From 4a06ea9e259f6e5e723b0095083e7e2e98b421ae Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 25 Oct 2024 12:16:31 -0700 Subject: [PATCH 17/17] more lax on circuit-json dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c132e3b..d2d12ae 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "peerDependencies": { "typescript": "^5.0.0", - "circuit-json": "^0.0.91" + "circuit-json": "*" }, "dependencies": { "fast-json-stable-stringify": "^2.1.0"