From b714f99de955f6111712bb74cafab6bf2e9c13a2 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 22:33:53 +0900 Subject: [PATCH] fix: draw silkscreen outlines for sod123 and sot723 (#734) --- src/fn/sod123.ts | 38 ++++++++++- src/fn/sot723.ts | 63 ++++++++++++++++- tests/__snapshots__/sod123.snap.svg | 2 +- tests/__snapshots__/sot723.snap.svg | 2 +- .../__snapshots__/sod123.snap.svg | 2 +- .../__snapshots__/sot-723.snap.svg | 2 +- .../diodes/__snapshots__/sod123.snap.svg | 2 +- tests/sod123.test.ts | 68 +++++++++++++++++++ 8 files changed, 172 insertions(+), 7 deletions(-) diff --git a/src/fn/sod123.ts b/src/fn/sod123.ts index 350726abb..22d77b85a 100644 --- a/src/fn/sod123.ts +++ b/src/fn/sod123.ts @@ -1,4 +1,8 @@ -import type { AnyCircuitElement, PcbCourtyardRect } from "circuit-json" +import type { + AnyCircuitElement, + PcbCourtyardRect, + PcbSilkscreenPath, +} from "circuit-json" import { z } from "zod" import { rectpad } from "../helpers/rectpad" import { silkscreenRef, type SilkscreenRef } from "src/helpers/silkscreenRef" @@ -39,9 +43,41 @@ export const sod123 = ( layer: "top", } + // Body outline. Every other member of the sod family draws one; sod123 was + // the only one shipping bare pads. Derived from the pads rather than from + // `h`, because `h` here is the body height (1.22mm) and the pads are taller + // than that (1.2mm tall, so ±0.6) — placing the runs at ±h/2 would leave + // 0.01mm and the stroke alone would put silk on copper. + const padHalfLength = length.parse(parameters.pl) / 2 + const padHalfWidth = length.parse(parameters.pw) / 2 + const pitch = length.parse(parameters.p) + const strokeWidth = 0.1 + const silkPadClearance = 0.2 + strokeWidth / 2 + + const outlineHalfHeight = padHalfWidth + silkPadClearance + // Left edge sits outboard of pad 1, marking the cathode end like KiCad's + // D_SOD-123 does; the right side stays open so it never crosses pad 2. + const outlineLeftX = -pitch / 2 - padHalfLength - silkPadClearance + const outlineRightX = pitch / 2 - padHalfLength - silkPadClearance + + const silkscreenOutline: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + route: [ + { x: outlineRightX, y: outlineHalfHeight }, + { x: outlineLeftX, y: outlineHalfHeight }, + { x: outlineLeftX, y: -outlineHalfHeight }, + { x: outlineRightX, y: -outlineHalfHeight }, + ], + stroke_width: strokeWidth, + } + return { circuitJson: sodWithoutParsing(parameters).concat( ...createFabricationNoteDiodeFromCopperPads(parameters), + silkscreenOutline as AnyCircuitElement, silkscreenRefText as AnyCircuitElement, courtyard as AnyCircuitElement, ), diff --git a/src/fn/sot723.ts b/src/fn/sot723.ts index a4adac332..7041b4b55 100644 --- a/src/fn/sot723.ts +++ b/src/fn/sot723.ts @@ -2,6 +2,7 @@ import { length, type AnyCircuitElement, type PcbCourtyardOutline, + type PcbSilkscreenPath, } from "circuit-json" import { z } from "zod" import { rectpad } from "../helpers/rectpad" @@ -44,8 +45,68 @@ export const sot723 = ( layer: "top", } + // Body outline. Every sibling SOT draws one; sot723 shipped bare pads. + // The pads reach past the body on both sides, so this emits the two runs + // that clear copper (above pin 1 / below pin 2) rather than a closed box, + // which is what KiCad's SOT-723 does. + const padHalfWidth = length.parse(parameters.pw) / 2 + const padHalfLength = length.parse(parameters.pl) / 2 + const pitch = length.parse(parameters.p) + const strokeWidth = 0.1 + const silkPadClearance = 0.2 + strokeWidth / 2 + + // Take the pad extents from the coordinate function rather than assuming + // them, so the outline follows if the geometry is ever parameterised. + const padCoords = [1, 2, 3].map((pn) => + getCcwSot723Coords({ + num_pins: parameters.num_pins, + pn, + w: length.parse(parameters.w), + h: length.parse(parameters.h), + pl: length.parse(parameters.pl), + p: pitch, + }), + ) + const padOuterY = + Math.max(...padCoords.map((c) => Math.abs(c.y))) + padHalfWidth + const runY = padOuterY + silkPadClearance + // The runs sit clear of every pad vertically, so they span the body width. + // Clamp anyway against any pad that does reach into the run's y band, so the + // outline stays correct if the geometry is ever parameterised differently. + const bodyHalfWidth = length.parse(parameters.w) / 2 + let runHalfX = bodyHalfWidth + for (const coord of padCoords) { + const padTop = coord.y + padHalfWidth + silkPadClearance + const padBottom = coord.y - padHalfWidth - silkPadClearance + // `>=` so a run that only grazes the clearance boundary — which is exactly + // where `runY` lands for the outermost pad — isn't treated as a collision. + if (runY >= padTop || runY <= padBottom) continue + runHalfX = Math.min( + runHalfX, + Math.max(0, Math.abs(coord.x) - padHalfLength - silkPadClearance), + ) + } + + const silkPath = (id: string, y: number): PcbSilkscreenPath => ({ + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: id, + route: [ + { x: -runHalfX, y }, + { x: runHalfX, y }, + ], + stroke_width: strokeWidth, + }) + return { - circuitJson: [...pad, silkscreenRefText as AnyCircuitElement, courtyard], + circuitJson: [ + ...pad, + silkPath("silkscreen_path_top", runY) as AnyCircuitElement, + silkPath("silkscreen_path_bottom", -runY) as AnyCircuitElement, + silkscreenRefText as AnyCircuitElement, + courtyard, + ], parameters, } } diff --git a/tests/__snapshots__/sod123.snap.svg b/tests/__snapshots__/sod123.snap.svg index ff3dc02a7..20c9c3d74 100644 --- a/tests/__snapshots__/sod123.snap.svg +++ b/tests/__snapshots__/sod123.snap.svg @@ -1 +1 @@ -{REF}+- \ No newline at end of file +{REF}+- \ No newline at end of file diff --git a/tests/__snapshots__/sot723.snap.svg b/tests/__snapshots__/sot723.snap.svg index d2d9e5af0..2835375d5 100644 --- a/tests/__snapshots__/sot723.snap.svg +++ b/tests/__snapshots__/sot723.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/sod123.snap.svg b/tests/kicad-parity/__snapshots__/sod123.snap.svg index 10bb7b3a6..bbb86bc57 100644 --- a/tests/kicad-parity/__snapshots__/sod123.snap.svg +++ b/tests/kicad-parity/__snapshots__/sod123.snap.svg @@ -1 +1 @@ -{REF}REF**Diff: 0.00%+- \ No newline at end of file +{REF}REF**Diff: 0.00%+- \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/sot-723.snap.svg b/tests/kicad-parity/__snapshots__/sot-723.snap.svg index 021fb0031..5a66baff3 100644 --- a/tests/kicad-parity/__snapshots__/sot-723.snap.svg +++ b/tests/kicad-parity/__snapshots__/sot-723.snap.svg @@ -1 +1 @@ -{REF}REF**Diff: 0.00% \ No newline at end of file +{REF}REF**Diff: 0.00% \ No newline at end of file diff --git a/tests/kicad-parity/diodes/__snapshots__/sod123.snap.svg b/tests/kicad-parity/diodes/__snapshots__/sod123.snap.svg index 10bb7b3a6..bbb86bc57 100644 --- a/tests/kicad-parity/diodes/__snapshots__/sod123.snap.svg +++ b/tests/kicad-parity/diodes/__snapshots__/sod123.snap.svg @@ -1 +1 @@ -{REF}REF**Diff: 0.00%+- \ No newline at end of file +{REF}REF**Diff: 0.00%+- \ No newline at end of file diff --git a/tests/sod123.test.ts b/tests/sod123.test.ts index fd2b7ad11..c1e9ffe5f 100644 --- a/tests/sod123.test.ts +++ b/tests/sod123.test.ts @@ -7,3 +7,71 @@ test("sod123", () => { const svgContent = convertCircuitJsonToPcbSvg(soup) expect(svgContent).toMatchSvgSnapshot(import.meta.path, "sod123") }) + +test("sod123 and sot723 draw a silkscreen outline that clears every pad", () => { + // Both were the only members of their families shipping bare pads: every + // other sod (sod123f/w/fl, sod323, sod523, sod923, sod128) and every other + // SOT already drew a body outline. + const footprints = [ + "sod123", + "sot723", + // Siblings, to prove the shared clearance rule isn't regressed. + "sod123f", + "sod123w", + "sod123fl", + "sod128", + "sod323", + ] + + const distanceToPad = (point: { x: number; y: number }, pad: any) => { + const dx = Math.max( + pad.x - pad.width / 2 - point.x, + 0, + point.x - (pad.x + pad.width / 2), + ) + const dy = Math.max( + pad.y - pad.height / 2 - point.y, + 0, + point.y - (pad.y + pad.height / 2), + ) + return Math.sqrt(dx * dx + dy * dy) + } + + for (const name of footprints) { + const circuitJson = fp.string(name).circuitJson() + const pads = circuitJson.filter((e) => e.type === "pcb_smtpad") as any[] + const paths = circuitJson.filter( + (e) => e.type === "pcb_silkscreen_path", + ) as any[] + + expect({ name, hasSilkscreen: paths.length > 0 }).toEqual({ + name, + hasSilkscreen: true, + }) + + let minClearance = Number.POSITIVE_INFINITY + for (const path of paths) { + const strokeHalfWidth = (path.stroke_width ?? 0) / 2 + for (let i = 0; i < path.route.length - 1; i++) { + const a = path.route[i] + const b = path.route[i + 1] + for (let t = 0; t <= 1; t += 0.01) { + const point = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t } + for (const pad of pads) { + minClearance = Math.min( + minClearance, + distanceToPad(point, pad) - strokeHalfWidth, + ) + } + } + } + } + + expect(Number.isFinite(minClearance)).toBe(true) + // IPC silkscreen-to-pad clearance, the same 0.2mm KiCad leaves. + expect({ name, clears: minClearance >= 0.2 - 1e-6 }).toEqual({ + name, + clears: true, + }) + } +})