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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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,
+ })
+ }
+})