diff --git a/src/fn/fpc.ts b/src/fn/fpc.ts index 692426f5..afa994d4 100644 --- a/src/fn/fpc.ts +++ b/src/fn/fpc.ts @@ -11,7 +11,7 @@ import { base_def } from "../helpers/zod/base_def" export const fpc_def = base_def.extend({ fn: z.literal("fpc"), - num_pins: z.coerce.number().int().min(2), + num_pins: z.coerce.number().int().min(2).default(12), p: length.default("0.5mm").describe("contact pad pitch"), pw: length.default("0.3mm").describe("contact pad width"), pl: length.default("1.25mm").describe("contact pad length"), diff --git a/tests/__snapshots__/fpc_default_12pin.snap.svg b/tests/__snapshots__/fpc_default_12pin.snap.svg new file mode 100644 index 00000000..1888a1b1 --- /dev/null +++ b/tests/__snapshots__/fpc_default_12pin.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/fpc.test.ts b/tests/fpc.test.ts index 26ee2572..8036893b 100644 --- a/tests/fpc.test.ts +++ b/tests/fpc.test.ts @@ -119,3 +119,35 @@ test("fpc31_staggered supports unequal row lengths for FPC-0.3HF-31PWBH10", () = "fpc31_staggered_FPC-0.3HF-31PWBH10", ) }) + +test("fpc without a pin count defaults to the 12-pin FPC-05F-12PH20 layout", () => { + const circuitJson = fp.string("fpc").circuitJson() + const pads = circuitJson.filter( + (element) => element.type === "pcb_smtpad", + ) as RectangularPad[] + + // Bare `fpc` used to throw a raw ZodError because num_pins had no default. + // It now renders the same 12-pin part the explicit fpc12 test asserts. + const reference = fp + .string( + "fpc12_p0.5mm_pw0.3mm_pl1.25mm_mpx8.88mm_mpy2.575mm_mpw2mm_mpl2.5mm", + ) + .circuitJson() + .filter((element) => element.type === "pcb_smtpad") as RectangularPad[] + + expect(pads).toHaveLength(14) + expect(pads).toHaveLength(reference.length) + pads.forEach((pad, index) => { + const referencePad = reference[index]! + expect(pad.x).toBeCloseTo(referencePad.x, 6) + expect(pad.y).toBeCloseTo(referencePad.y, 6) + expect(pad.width).toBeCloseTo(referencePad.width, 6) + expect(pad.height).toBeCloseTo(referencePad.height, 6) + expect(pad.port_hints).toEqual(referencePad.port_hints) + }) + + expect(convertCircuitJsonToPcbSvg(circuitJson)).toMatchSvgSnapshot( + import.meta.path, + "fpc_default_12pin", + ) +}) diff --git a/tests/kicad-parity/__snapshots__/fpc_default_vs_kicad_fh12.snap.svg b/tests/kicad-parity/__snapshots__/fpc_default_vs_kicad_fh12.snap.svg new file mode 100644 index 00000000..d944f1c4 --- /dev/null +++ b/tests/kicad-parity/__snapshots__/fpc_default_vs_kicad_fh12.snap.svg @@ -0,0 +1 @@ +{REF}REF**Hirose_FH12-12S-0.5SH_1x12-1MP_P0.50mm_HorizontalREF** \ No newline at end of file diff --git a/tests/kicad-parity/fpc_kicad_parity.test.ts b/tests/kicad-parity/fpc_kicad_parity.test.ts new file mode 100644 index 00000000..a38d4179 --- /dev/null +++ b/tests/kicad-parity/fpc_kicad_parity.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { transformPcbElements } from "@tscircuit/circuit-json-util" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { translate } from "transformation-matrix" +import { fp } from "src/footprinter" + +// KiCad has no single canonical FPC footprint. Every part in +// Connector_FFC-FPC.pretty is a specific vendor connector (Hirose, Amphenol, +// TE, ...) with its own housing, mounting-pad geometry and courtyard, so the +// usual whole-footprint courtyard-parity assertion does not apply to a bare +// `fpc`. What every 0.5mm 12-pin FFC part does share is the contact land +// pattern: 12 pads on a 0.5mm pitch, 0.3mm wide, centered on the origin. This +// test pins footprinter's default `fpc` contacts to that shared KiCad land +// pattern, using Hirose FH12-12S-0.5SH as the reference, then overlays both for +// a visual snapshot. +// +// It deliberately does not assert courtyard parity. Footprinter draws a +// pad-bounding-box courtyard (11.38 x 4.95mm); the KiCad courtyard encloses the +// connector body (12.12 x 7.9mm). Measured courtyard IoU against the nearest +// KiCad 12-pin 0.5mm parts is 47-59% (diff 41-53%), driven by the housing, which +// footprinter does not model and which differs by vendor. +const KICAD_PATH = + "Connector_FFC-FPC.pretty/Hirose_FH12-12S-0.5SH_1x12-1MP_P0.50mm_Horizontal.circuit.json" +const KICAD_URL = `https://kicad-mod-cache.tscircuit.com/${KICAD_PATH}` + +// Contacts are the narrow pads (<= 1mm wide). The two mounting pads are wider +// (2mm in footprinter, 1.8mm in KiCad) and vendor-specific, so they are excluded. +const contactPads = (elements: any[]): any[] => + elements + .filter((e) => e.type === "pcb_smtpad" && e.width <= 1) + .sort((a, b) => a.x - b.x) + +const pitchOf = (pads: any[]): number => + (pads[pads.length - 1].x - pads[0].x) / (pads.length - 1) + +test("parity/fpc default contacts match KiCad 0.5mm 12-pin FFC land pattern", async () => { + const res = await fetch(KICAD_URL) + expect(res.ok).toBe(true) + const kicadJson = (await res.json()) as any[] + + const fpcJson = fp.string("fpc").circuitJson() as any[] + + const fpcContacts = contactPads(fpcJson) + const kicadContacts = contactPads(kicadJson) + + // Same contact count, pitch and pad width as the KiCad land pattern. + expect(fpcContacts).toHaveLength(12) + expect(kicadContacts).toHaveLength(12) + expect(pitchOf(fpcContacts)).toBeCloseTo(0.5, 6) + expect(pitchOf(fpcContacts)).toBeCloseTo(pitchOf(kicadContacts), 6) + + // Identical contact X positions (both centered on the origin) and pad width. + fpcContacts.forEach((pad, index) => { + expect(pad.x).toBeCloseTo(kicadContacts[index]!.x, 6) + expect(pad.width).toBeCloseTo(kicadContacts[index]!.width, 6) + }) + + // Overlay footprinter (left) against the KiCad reference (shifted right). + const kicadShifted = transformPcbElements(kicadJson as any, translate(16, 0)) + const svg = convertCircuitJsonToPcbSvg([...fpcJson, ...kicadShifted], { + showCourtyards: true, + }) + expect(svg).toMatchSvgSnapshot(import.meta.path, "fpc_default_vs_kicad_fh12") +}, 15000)