Add explicit SSOP body envelope dimensions - #886
Conversation
| import { expect, test } from "bun:test" | ||
| import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" | ||
| import { fp } from "../src/footprinter" | ||
|
|
||
| test("ssop body outline expands the courtyard without changing copper or silkscreen", () => { | ||
| const base = "ssop28_p0.65mm_w8.93mm" | ||
| const original = fp.string(base).circuitJson() | ||
| for (const [name, suffix, minX, minY] of [ | ||
| [ | ||
| "nominal", | ||
| "_bodywidth5.3mm_bodyheight10.2mm_bodythickness1.85mm", | ||
| 5.3 / 2 + 0.25, | ||
| 10.2 / 2 + 0.25, | ||
| ], | ||
| ["large-body", "_bodywidth12mm_bodyheight14mm", 6.25, 7.25], | ||
| ["width-only", "_bodywidth12mm", 6.25, 0], | ||
| ["height-only", "_bodyheight14mm", 0, 7.25], | ||
| ] as const) { | ||
| const elements = fp.string(base + suffix).circuitJson() | ||
| expect(elements.filter((e) => e.type !== "pcb_courtyard_outline")).toEqual( | ||
| original.filter((e) => e.type !== "pcb_courtyard_outline"), | ||
| ) | ||
| const courtyard = elements.find((e) => e.type === "pcb_courtyard_outline")! | ||
| if (courtyard.type !== "pcb_courtyard_outline") | ||
| throw new Error("missing courtyard") | ||
| const hx = Math.max(...courtyard.outline.map((p) => p.x)) | ||
| const hy = Math.max(...courtyard.outline.map((p) => p.y)) | ||
| expect(hx).toBeGreaterThanOrEqual(minX) | ||
| expect(hy).toBeGreaterThanOrEqual(minY) | ||
| for (const pad of elements) { | ||
| if (pad.type !== "pcb_smtpad" || pad.shape !== "rect") continue | ||
| expect(hx + 1e-9).toBeGreaterThanOrEqual( | ||
| Math.abs(pad.x) + pad.width / 2 + 0.25, | ||
| ) | ||
| expect(hy + 1e-9).toBeGreaterThanOrEqual( | ||
| Math.abs(pad.y) + pad.height / 2 + 0.25, | ||
| ) | ||
| } | ||
| expect( | ||
| convertCircuitJsonToPcbSvg(elements, { | ||
| showCourtyards: true, | ||
| viewport: { minX: -hx - 1, maxX: hx + 1, minY: -hy - 1, maxY: hy + 1 }, | ||
| }), | ||
| ).toMatchSvgSnapshot(import.meta.path, "ssop-body-" + name) | ||
| } | ||
| }) |
There was a problem hiding this comment.
This file contains a single test(...) block that internally iterates over four distinct scenarios ('nominal', 'large-body', 'width-only', 'height-only'), each producing its own SVG snapshot assertion. The style guide rule states that a *.test.ts file may have AT MOST one test(...), and after that the user should split into multiple, numbered files (e.g. add1.test.ts, add2.test.ts). While there is technically only one test() call, the loop drives four independent test cases that should each live in their own file. This file should be split into e.g. ssop-body-envelope1.test.ts (nominal), ssop-body-envelope2.test.ts (large-body), ssop-body-envelope3.test.ts (width-only), and ssop-body-envelope4.test.ts (height-only), each with its own test(...) call.
| import { expect, test } from "bun:test" | |
| import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" | |
| import { fp } from "../src/footprinter" | |
| test("ssop body outline expands the courtyard without changing copper or silkscreen", () => { | |
| const base = "ssop28_p0.65mm_w8.93mm" | |
| const original = fp.string(base).circuitJson() | |
| for (const [name, suffix, minX, minY] of [ | |
| [ | |
| "nominal", | |
| "_bodywidth5.3mm_bodyheight10.2mm_bodythickness1.85mm", | |
| 5.3 / 2 + 0.25, | |
| 10.2 / 2 + 0.25, | |
| ], | |
| ["large-body", "_bodywidth12mm_bodyheight14mm", 6.25, 7.25], | |
| ["width-only", "_bodywidth12mm", 6.25, 0], | |
| ["height-only", "_bodyheight14mm", 0, 7.25], | |
| ] as const) { | |
| const elements = fp.string(base + suffix).circuitJson() | |
| expect(elements.filter((e) => e.type !== "pcb_courtyard_outline")).toEqual( | |
| original.filter((e) => e.type !== "pcb_courtyard_outline"), | |
| ) | |
| const courtyard = elements.find((e) => e.type === "pcb_courtyard_outline")! | |
| if (courtyard.type !== "pcb_courtyard_outline") | |
| throw new Error("missing courtyard") | |
| const hx = Math.max(...courtyard.outline.map((p) => p.x)) | |
| const hy = Math.max(...courtyard.outline.map((p) => p.y)) | |
| expect(hx).toBeGreaterThanOrEqual(minX) | |
| expect(hy).toBeGreaterThanOrEqual(minY) | |
| for (const pad of elements) { | |
| if (pad.type !== "pcb_smtpad" || pad.shape !== "rect") continue | |
| expect(hx + 1e-9).toBeGreaterThanOrEqual( | |
| Math.abs(pad.x) + pad.width / 2 + 0.25, | |
| ) | |
| expect(hy + 1e-9).toBeGreaterThanOrEqual( | |
| Math.abs(pad.y) + pad.height / 2 + 0.25, | |
| ) | |
| } | |
| expect( | |
| convertCircuitJsonToPcbSvg(elements, { | |
| showCourtyards: true, | |
| viewport: { minX: -hx - 1, maxX: hx + 1, minY: -hy - 1, maxY: hy + 1 }, | |
| }), | |
| ).toMatchSvgSnapshot(import.meta.path, "ssop-body-" + name) | |
| } | |
| }) | |
| import { expect, test } from "bun:test" | |
| import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" | |
| import { fp } from "../src/footprinter" | |
| test("ssop body outline expands the courtyard without changing copper or silkscreen (nominal)", () => { | |
| const base = "ssop28_p0.65mm_w8.93mm" | |
| const original = fp.string(base).circuitJson() | |
| const name = "nominal" | |
| const suffix = "_bodywidth5.3mm_bodyheight10.2mm_bodythickness1.85mm" | |
| const minX = 5.3 / 2 + 0.25 | |
| const minY = 10.2 / 2 + 0.25 | |
| const elements = fp.string(base + suffix).circuitJson() | |
| expect(elements.filter((e) => e.type !== "pcb_courtyard_outline")).toEqual( | |
| original.filter((e) => e.type !== "pcb_courtyard_outline"), | |
| ) | |
| const courtyard = elements.find((e) => e.type === "pcb_courtyard_outline")! | |
| if (courtyard.type !== "pcb_courtyard_outline") | |
| throw new Error("missing courtyard") | |
| const hx = Math.max(...courtyard.outline.map((p) => p.x)) | |
| const hy = Math.max(...courtyard.outline.map((p) => p.y)) | |
| expect(hx).toBeGreaterThanOrEqual(minX) | |
| expect(hy).toBeGreaterThanOrEqual(minY) | |
| for (const pad of elements) { | |
| if (pad.type !== "pcb_smtpad" || pad.shape !== "rect") continue | |
| expect(hx + 1e-9).toBeGreaterThanOrEqual( | |
| Math.abs(pad.x) + pad.width / 2 + 0.25, | |
| ) | |
| expect(hy + 1e-9).toBeGreaterThanOrEqual( | |
| Math.abs(pad.y) + pad.height / 2 + 0.25, | |
| ) | |
| } | |
| expect( | |
| convertCircuitJsonToPcbSvg(elements, { | |
| showCourtyards: true, | |
| viewport: { minX: -hx - 1, maxX: hx + 1, minY: -hy - 1, maxY: hy + 1 }, | |
| }), | |
| ).toMatchSvgSnapshot(import.meta.path, "ssop-body-" + name) | |
| }) | |
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
| import { expect, test } from "bun:test" | ||
| import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" | ||
| import { fp } from "../src/footprinter" | ||
| import { ssop_def } from "../src/fn/ssop" | ||
|
|
||
| test("ssop body dimensions are validated schema fields with typed builder and unit parity", () => { | ||
| const base = "ssop28_p0.65mm_w8.93mm" | ||
| const suffix = "_bodywidth5.3mm_bodyheight10.2mm_bodythickness1.85mm" | ||
| const generated = fp.string(base + suffix) | ||
| expect(generated.json()).toMatchObject({ | ||
| bodywidth: 5.3, | ||
| bodyheight: 10.2, | ||
| bodythickness: 1.85, | ||
| }) | ||
| const builder = fp() | ||
| .ssop(28) | ||
| .p(0.65) | ||
| .w(8.93) | ||
| .bodywidth(5.3) | ||
| .bodyheight(10.2) | ||
| .bodythickness(1.85) | ||
| expect(builder.circuitJson()).toEqual(generated.circuitJson()) | ||
| const parsed = ssop_def.parse({ | ||
| fn: "ssop", | ||
| bodywidth: "100mil", | ||
| bodyheight: "0.1in", | ||
| bodythickness: "0.1cm", | ||
| }) | ||
| expect(parsed.bodywidth).toBeCloseTo(2.54) | ||
| expect(parsed.bodyheight).toBeCloseTo(2.54) | ||
| expect(parsed.bodythickness).toBeCloseTo(1) | ||
| // Z metadata never moves pads or changes the 2D outline. | ||
| expect(fp.string(base + "_bodythickness1mm").circuitJson()).toEqual( | ||
| fp.string(base).circuitJson(), | ||
| ) | ||
| for (const key of ["bodywidth", "bodyheight", "bodythickness"] as const) { | ||
| expect(ssop_def.parse({ fn: "ssop" })[key]).toBeUndefined() | ||
| } | ||
| expect( | ||
| convertCircuitJsonToPcbSvg(generated.circuitJson(), { | ||
| showCourtyards: true, | ||
| }), | ||
| ).toMatchSvgSnapshot(import.meta.path, "ssop-body-parameters") | ||
| }) |
There was a problem hiding this comment.
This file's single test(...) block covers multiple independent concerns: (1) string-based parameter parsing, (2) typed builder parity, (3) unit conversion (mil/in/cm), (4) the Z-metadata invariant (bodythickness does not affect 2D geometry), and (5) an SVG snapshot. The style guide rule states that a *.test.ts file may have AT MOST one test(...), and after that the user should split into multiple, numbered files. Because these are clearly distinct test scenarios bundled into one test, this file should be split into numbered files such as ssop-body-parameters1.test.ts, ssop-body-parameters2.test.ts, etc., each containing a single focused test(...) call.
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
|
Thank you for your contribution! 🎉 PR Rating: ⭐⭐⭐ Track your contributions and see the leaderboard at: tscircuit Contribution Tracker |
SSOP's existing
wdetermines pad-row placement and does not identify the physical molded body. Add optional, positive finitebodywidth,bodyheight, andbodythicknesslengths to the SSOP schema and typed builder.bodywidthis physical X andbodyheightphysical Y, before footprint rotation, following the board-plane naming proposed in #866.bodythicknessis body Z size, excluding board standoff; it is validated metadata for downstream 3D consumers and does not change 2D geometry. Omitted dimensions remain absent.When a body outline is supplied, use a rectangular courtyard enclosing both body and actual copper with 0.25mm clearance, including offset thermal pads. An omitted axis retains its legacy bounds. Pad geometry, numbering, and silkscreen remain unchanged; existing strings retain their original courtyard. No manufacturer names or model selectors are introduced.
The example describes an MO-150 SSOP body, such as the package compared against JLC C136617. It supplies the mold envelope needed by the merged SSOP model; physical lead dimensions and standoff are separate future spec work. Footprinter3d integration follows after the schema is accepted and released. This branch is independent of the LGA proposal #885 and can merge separately.
Validation: four new tests cover SVG snapshots, typed builder/string parity, units, invalid/missing lengths, independent axes, thickness-only geometry invariance, and offset thermal-pad clearance. Full suite: 568 pass, zero failures. Build passes; typecheck output is identical to main's existing errors.
CI fix: refresh the QFN32 KiCad comparison snapshot after reviewing the current reference pin-1 silkscreen marker. Add an explicit zero copper-difference assertion; the boolean copper snapshot remains unchanged. The same fix is applied to both body-dimension PRs so they remain independently mergeable.