From 167d923bb4fe7625d42e7c4d379875f73a303eb2 Mon Sep 17 00:00:00 2001 From: buildingvibes Date: Tue, 10 Feb 2026 15:11:50 -0800 Subject: [PATCH 1/3] feat: implement PDIP footprint (Plastic DIP) Add PDIP (Plastic Dual In-line Package) as a footprint variant that delegates to the existing DIP implementation. PDIP is the most common DIP package variant and uses the same dimensions: 300mil (7.62mm) row spacing with 2.54mm pin pitch. Supports any even pin count via the string parser (e.g. pdip8, pdip16, pdip28) as well as the builder API. Closes #371 --- src/fn/index.ts | 1 + src/fn/pdip.ts | 19 ++++++++++ src/footprinter.ts | 3 ++ tests/__snapshots__/pdip16.snap.svg | 1 + tests/__snapshots__/pdip8.snap.svg | 1 + tests/pdip.test.ts | 58 +++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+) create mode 100644 src/fn/pdip.ts create mode 100644 tests/__snapshots__/pdip16.snap.svg create mode 100644 tests/__snapshots__/pdip8.snap.svg create mode 100644 tests/pdip.test.ts diff --git a/src/fn/index.ts b/src/fn/index.ts index c3ee18a1..db3b4a9c 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -1,4 +1,5 @@ export { dip } from "./dip" +export { pdip } from "./pdip" export { diode } from "./diode" export { cap } from "./cap" export { led } from "./led" diff --git a/src/fn/pdip.ts b/src/fn/pdip.ts new file mode 100644 index 00000000..c55e134f --- /dev/null +++ b/src/fn/pdip.ts @@ -0,0 +1,19 @@ +import type { AnyCircuitElement } from "circuit-json" +import { dip, dip_def } from "./dip" + +/** + * PDIP (Plastic Dual In-line Package) footprint. + * + * PDIP is the most common DIP variant with a plastic body. The footprint is + * identical to a standard DIP: 300mil (7.62mm) row spacing, 2.54mm pitch. + * + * Supports any even pin count (e.g. pdip4, pdip8, pdip14, pdip16, pdip20, pdip28). + */ +export const pdip = ( + raw_params: Record, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + const params = { ...raw_params, dip: true, fn: "dip" } + // Remove the pdip flag so dip_def.parse won't choke + delete params.pdip + return dip(params as any) +} diff --git a/src/footprinter.ts b/src/footprinter.ts index 259d1d27..4787a1ce 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -39,6 +39,9 @@ export type Footprinter = { dip: ( num_pins?: number, ) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow"> + pdip: ( + num_pins?: number, + ) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow"> cap: () => FootprinterParamsBuilder res: () => FootprinterParamsBuilder diode: () => FootprinterParamsBuilder diff --git a/tests/__snapshots__/pdip16.snap.svg b/tests/__snapshots__/pdip16.snap.svg new file mode 100644 index 00000000..595c0386 --- /dev/null +++ b/tests/__snapshots__/pdip16.snap.svg @@ -0,0 +1 @@ +{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8}{pin9}{pin10}{pin11}{pin12}{pin13}{pin14}{pin15}{pin16} \ No newline at end of file diff --git a/tests/__snapshots__/pdip8.snap.svg b/tests/__snapshots__/pdip8.snap.svg new file mode 100644 index 00000000..4a51d472 --- /dev/null +++ b/tests/__snapshots__/pdip8.snap.svg @@ -0,0 +1 @@ +{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file diff --git a/tests/pdip.test.ts b/tests/pdip.test.ts new file mode 100644 index 00000000..eaa81b1d --- /dev/null +++ b/tests/pdip.test.ts @@ -0,0 +1,58 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" +import type { AnyCircuitElement } from "circuit-json" + +test("pdip8", () => { + const circuitJson = fp.string("pdip8").circuitJson() as AnyCircuitElement[] + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip8") +}) + +test("pdip8 matches dip8", () => { + const pdipJson = fp.string("pdip8").circuitJson() as AnyCircuitElement[] + const dipJson = fp.string("dip8").circuitJson() as AnyCircuitElement[] + expect(pdipJson).toEqual(dipJson) +}) + +test("pdip8 parameters", () => { + const json = fp.string("pdip8").json() + expect(json).toMatchInlineSnapshot( + { + fn: "dip", + id: 0.8, + num_pins: 8, + od: 1.6, + p: 2.54, + w: 7.62, + }, + ` +{ + "fn": "dip", + "id": 0.8, + "nosquareplating": false, + "num_pins": 8, + "od": 1.6, + "p": 2.54, + "w": 7.62, +} +`, + ) +}) + +test("pdip16", () => { + const circuitJson = fp.string("pdip16").circuitJson() as AnyCircuitElement[] + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip16") +}) + +test("pdip16 matches dip16", () => { + const pdipJson = fp.string("pdip16").circuitJson() as AnyCircuitElement[] + const dipJson = fp.string("dip16").circuitJson() as AnyCircuitElement[] + expect(pdipJson).toEqual(dipJson) +}) + +test("pdip builder API", () => { + const circuitJson = fp().pdip(8).w(7.62).circuitJson() + expect(circuitJson.length).toBeGreaterThan(0) +}) From 2f8a5ce8e9cedda806f0d297bcb1690cd2dea5ae Mon Sep 17 00:00:00 2001 From: buildingvibes Date: Wed, 11 Feb 2026 22:31:55 -0800 Subject: [PATCH 2/3] test: add KiCad parity test for PDIP-8 Add parity test comparing pdip8 footprinter output against KiCad's DIP-8_W7.62mm reference footprint. The test confirms 0% difference in pad/hole areas between the two implementations. --- .../kicad-parity/__snapshots__/pdip8.snap.svg | 1 + .../pdip8_boolean_difference.snap.svg | 3 +++ tests/kicad-parity/pdip8_kicad_parity.test.ts | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 tests/kicad-parity/__snapshots__/pdip8.snap.svg create mode 100644 tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg create mode 100644 tests/kicad-parity/pdip8_kicad_parity.test.ts diff --git a/tests/kicad-parity/__snapshots__/pdip8.snap.svg b/tests/kicad-parity/__snapshots__/pdip8.snap.svg new file mode 100644 index 00000000..a0d295df --- /dev/null +++ b/tests/kicad-parity/__snapshots__/pdip8.snap.svg @@ -0,0 +1 @@ +{REF}Diff: 0.00%{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg new file mode 100644 index 00000000..d4260e91 --- /dev/null +++ b/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg @@ -0,0 +1,3 @@ +DIP-8_W7.62mm - Alignment Analysis (Footprinter vs KiCad)pdip8KiCad: DIP-8_W7.62mmPerfect alignment = complete overlap \ No newline at end of file diff --git a/tests/kicad-parity/pdip8_kicad_parity.test.ts b/tests/kicad-parity/pdip8_kicad_parity.test.ts new file mode 100644 index 00000000..e07e0c59 --- /dev/null +++ b/tests/kicad-parity/pdip8_kicad_parity.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" + +test("parity/pdip8", async () => { + const { avgRelDiff, combinedFootprintElements, booleanDifferenceSvg } = + await compareFootprinterVsKicad( + "pdip8", + "Package_DIP.pretty/DIP-8_W7.62mm.circuit.json", + ) + + const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip8") + expect(booleanDifferenceSvg).toMatchSvgSnapshot( + import.meta.path, + "pdip8_boolean_difference", + ) +}) From 2ea6e2dd7f0bf21c6a73af6c1cfdaf9a3b0bda2c Mon Sep 17 00:00:00 2001 From: buildingvibes Date: Thu, 12 Feb 2026 09:50:15 -0800 Subject: [PATCH 3/3] fix: differentiate PDIP from DIP with oblong pads and KiCad parity test Address review feedback that PDIP and DIP are not the same package. PDIP now uses oblong/pill-shaped pads (2.4mm x 1.6mm) instead of circular pads (1.6mm diameter), matching KiCad's DIP-8_W7.62mm_LongPads reference footprint. Updated KiCad parity test to compare against the correct LongPads variant. Co-Authored-By: Claude Opus 4.6 --- src/fn/pdip.ts | 128 +++++++++++++++++- tests/__snapshots__/pdip16.snap.svg | 2 +- tests/__snapshots__/pdip8.snap.svg | 2 +- .../kicad-parity/__snapshots__/pdip8.snap.svg | 2 +- .../pdip8_boolean_difference.snap.svg | 6 +- tests/kicad-parity/pdip8_kicad_parity.test.ts | 2 +- tests/pdip.test.ts | 28 ++-- 7 files changed, 146 insertions(+), 24 deletions(-) diff --git a/src/fn/pdip.ts b/src/fn/pdip.ts index c55e134f..cac54729 100644 --- a/src/fn/pdip.ts +++ b/src/fn/pdip.ts @@ -1,19 +1,133 @@ -import type { AnyCircuitElement } from "circuit-json" -import { dip, dip_def } from "./dip" +import type { + AnyCircuitElement, + PcbFabricationNoteText, + PcbSilkscreenPath, +} from "circuit-json" +import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef" +import { platedHolePill } from "../helpers/platedHolePill" +import { platedHoleWithRectPad } from "../helpers/platedHoleWithRectPad" +import { u_curve } from "../helpers/u-curve" +import { getCcwDipCoords, extendDipDef } from "./dip" /** * PDIP (Plastic Dual In-line Package) footprint. * - * PDIP is the most common DIP variant with a plastic body. The footprint is - * identical to a standard DIP: 300mil (7.62mm) row spacing, 2.54mm pitch. + * PDIP differs from standard DIP by using oblong/oval pads instead of + * circular pads. Typical PDIP-8 dimensions (matching KiCad DIP-8_W7.62mm_LongPads): + * - Row spacing (w): 7.62mm (300mil) + * - Pin pitch (p): 2.54mm (100mil) + * - Hole diameter (id): 0.8mm + * - Pad width (ow): 2.4mm (horizontal, along row direction) + * - Pad height (oh): 1.6mm (vertical, along pitch direction) * * Supports any even pin count (e.g. pdip4, pdip8, pdip14, pdip16, pdip20, pdip28). */ + +export const pdip_def = extendDipDef({}) + export const pdip = ( raw_params: Record, ): { circuitJson: AnyCircuitElement[]; parameters: any } => { - const params = { ...raw_params, dip: true, fn: "dip" } - // Remove the pdip flag so dip_def.parse won't choke + const params = { ...raw_params, fn: "pdip" } delete params.pdip - return dip(params as any) + const parameters = pdip_def.parse(params) + + // PDIP uses oblong pads: wider horizontally than vertically + // Default outer width is 1.5x the outer diameter (od) + const padWidth = parameters.od * 1.5 // 2.4mm for default od=1.6mm + const padHeight = parameters.od // 1.6mm + + const platedHoles: AnyCircuitElement[] = [] + for (let i = 0; i < parameters.num_pins; i++) { + const { x, y } = getCcwDipCoords( + parameters.num_pins, + i + 1, + parameters.w, + parameters.p ?? 2.54, + parameters.nosquareplating, + ) + if (i === 0 && !parameters.nosquareplating) { + // Pin 1 uses a rectangular pad (same oblong dimensions) + platedHoles.push( + platedHoleWithRectPad({ + pn: i + 1, + x, + y, + holeDiameter: parameters.id ?? "0.8mm", + rectPadWidth: padWidth, + rectPadHeight: padHeight, + }), + ) + continue + } + // Non-pin-1 pads use pill (oblong) shape + platedHoles.push( + platedHolePill(i + 1, x, y, parameters.id ?? 0.8, padWidth, padHeight), + ) + } + + // Silkscreen uses the wider pad dimension for clearance + const innerGap = parameters.w - padWidth + const sw = innerGap - 1 + + const sh = (parameters.num_pins / 2 - 1) * parameters.p + padHeight + 0.4 + + const silkscreenBorder: PcbSilkscreenPath = { + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_path_1", + route: [ + { x: -sw / 2, y: -sh / 2 }, + { x: -sw / 2, y: sh / 2 }, + ...u_curve.map(({ x, y }) => ({ + x: (x * sw) / 6, + y: (y * sw) / 6 + sh / 2, + })), + { x: sw / 2, y: sh / 2 }, + { x: sw / 2, y: -sh / 2 }, + { x: -sw / 2, y: -sh / 2 }, + ], + type: "pcb_silkscreen_path", + stroke_width: 0.1, + } + + const silkscreenPins: PcbFabricationNoteText[] = [] + for (let i = 0; i < parameters.num_pins; i++) { + const isLeft = i < parameters.num_pins / 2 + const clearance = 0.6 + const { y: pinCenterY } = getCcwDipCoords( + parameters.num_pins, + i + 1, + parameters.w, + parameters.p ?? 2.54, + parameters.nosquareplating, + ) + const pinLabelX = isLeft + ? -parameters.w / 2 - padWidth / 2 - clearance + : parameters.w / 2 + padWidth / 2 + clearance + const pinLabelY = pinCenterY + silkscreenPins.push({ + type: "pcb_fabrication_note_text", + pcb_fabrication_note_text_id: `pin_${i + 1}`, + layer: "top", + pcb_component_id: `pin_${i + 1}`, + text: `{pin${i + 1}}`, + anchor_position: { x: pinLabelX, y: pinLabelY }, + font_size: 0.3, + font: "tscircuit2024", + anchor_alignment: "top_left", + }) + } + + const silkscreenRefText: SilkscreenRef = silkscreenRef(0, sh / 2 + 0.5, 0.4) + + return { + circuitJson: [ + ...platedHoles, + silkscreenBorder, + silkscreenRefText, + ...silkscreenPins, + ], + parameters, + } } diff --git a/tests/__snapshots__/pdip16.snap.svg b/tests/__snapshots__/pdip16.snap.svg index 595c0386..197a7d51 100644 --- a/tests/__snapshots__/pdip16.snap.svg +++ b/tests/__snapshots__/pdip16.snap.svg @@ -1 +1 @@ -{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8}{pin9}{pin10}{pin11}{pin12}{pin13}{pin14}{pin15}{pin16} \ No newline at end of file +{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8}{pin9}{pin10}{pin11}{pin12}{pin13}{pin14}{pin15}{pin16} \ No newline at end of file diff --git a/tests/__snapshots__/pdip8.snap.svg b/tests/__snapshots__/pdip8.snap.svg index 4a51d472..1635dc02 100644 --- a/tests/__snapshots__/pdip8.snap.svg +++ b/tests/__snapshots__/pdip8.snap.svg @@ -1 +1 @@ -{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file +{REF}{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/pdip8.snap.svg b/tests/kicad-parity/__snapshots__/pdip8.snap.svg index a0d295df..8ac08aae 100644 --- a/tests/kicad-parity/__snapshots__/pdip8.snap.svg +++ b/tests/kicad-parity/__snapshots__/pdip8.snap.svg @@ -1 +1 @@ -{REF}Diff: 0.00%{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file +{REF}Diff: 0.00%{pin1}{pin2}{pin3}{pin4}{pin5}{pin6}{pin7}{pin8} \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg index d4260e91..09e3ee0a 100644 --- a/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg +++ b/tests/kicad-parity/__snapshots__/pdip8_boolean_difference.snap.svg @@ -1,3 +1,3 @@ -DIP-8_W7.62mm - Alignment Analysis (Footprinter vs KiCad)pdip8KiCad: DIP-8_W7.62mmPerfect alignment = complete overlap \ No newline at end of file +DIP-8_W7.62mm_LongPads - Alignment Analysis (Footprinter vs KiCad)pdip8KiCad: DIP-8_W7.62mm_LongPadsPerfect alignment = complete overlap \ No newline at end of file diff --git a/tests/kicad-parity/pdip8_kicad_parity.test.ts b/tests/kicad-parity/pdip8_kicad_parity.test.ts index e07e0c59..24eb1e1d 100644 --- a/tests/kicad-parity/pdip8_kicad_parity.test.ts +++ b/tests/kicad-parity/pdip8_kicad_parity.test.ts @@ -6,7 +6,7 @@ test("parity/pdip8", async () => { const { avgRelDiff, combinedFootprintElements, booleanDifferenceSvg } = await compareFootprinterVsKicad( "pdip8", - "Package_DIP.pretty/DIP-8_W7.62mm.circuit.json", + "Package_DIP.pretty/DIP-8_W7.62mm_LongPads.circuit.json", ) const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements) diff --git a/tests/pdip.test.ts b/tests/pdip.test.ts index eaa81b1d..542ee1d3 100644 --- a/tests/pdip.test.ts +++ b/tests/pdip.test.ts @@ -9,17 +9,31 @@ test("pdip8", () => { expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip8") }) -test("pdip8 matches dip8", () => { +test("pdip8 uses oblong pads unlike dip8 circular pads", () => { const pdipJson = fp.string("pdip8").circuitJson() as AnyCircuitElement[] const dipJson = fp.string("dip8").circuitJson() as AnyCircuitElement[] - expect(pdipJson).toEqual(dipJson) + + // PDIP should use pill-shaped pads for non-pin-1 holes + const pdipHoles = pdipJson.filter( + (e) => e.type === "pcb_plated_hole" && e.shape === "pill", + ) + const dipHoles = dipJson.filter( + (e) => e.type === "pcb_plated_hole" && e.shape === "circle", + ) + + // PDIP has pill pads, DIP has circle pads (for non-pin-1) + expect(pdipHoles.length).toBe(7) + expect(dipHoles.length).toBe(7) + + // PDIP and DIP should NOT produce identical output + expect(pdipJson).not.toEqual(dipJson) }) test("pdip8 parameters", () => { const json = fp.string("pdip8").json() expect(json).toMatchInlineSnapshot( { - fn: "dip", + fn: "pdip", id: 0.8, num_pins: 8, od: 1.6, @@ -28,7 +42,7 @@ test("pdip8 parameters", () => { }, ` { - "fn": "dip", + "fn": "pdip", "id": 0.8, "nosquareplating": false, "num_pins": 8, @@ -46,12 +60,6 @@ test("pdip16", () => { expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip16") }) -test("pdip16 matches dip16", () => { - const pdipJson = fp.string("pdip16").circuitJson() as AnyCircuitElement[] - const dipJson = fp.string("dip16").circuitJson() as AnyCircuitElement[] - expect(pdipJson).toEqual(dipJson) -}) - test("pdip builder API", () => { const circuitJson = fp().pdip(8).w(7.62).circuitJson() expect(circuitJson.length).toBeGreaterThan(0)