-
Notifications
You must be signed in to change notification settings - Fork 63
feat: implement chipled footprint with polarity indicator for chip LED packages #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| import type { | ||
| AnyCircuitElement, | ||
| PcbCourtyardRect, | ||
| PcbSilkscreenPath, | ||
| } from "circuit-json" | ||
| import { rectpad } from "../helpers/rectpad" | ||
| import { silkscreenRef, type SilkscreenRef } from "../helpers/silkscreenRef" | ||
| import { footprintSizes } from "../helpers/passive-fn" | ||
| import { z } from "zod" | ||
| import { base_def } from "../helpers/zod/base_def" | ||
|
|
||
| /** | ||
| * Chip LED footprint (chipled) - SMD LED with polarity indicator on silkscreen. | ||
| * | ||
| * Pin 1 = K (Cathode), Pin 2 = A (Anode). | ||
| * A vertical bar is drawn on the cathode side of the silkscreen outline. | ||
| * | ||
| * Supports standard imperial sizes: 0201, 0402, 0603, 0805, 1206, etc. | ||
| */ | ||
|
|
||
| type ChipLedSize = { | ||
| imperial: string | ||
| p_mm: number // pad center-to-center | ||
| pw_mm: number // pad width (along the component axis) | ||
| ph_mm: number // pad height (across the component axis) | ||
| body_w_mm: number // body width | ||
| body_h_mm: number // body height | ||
| } | ||
|
|
||
| const chipLedSizes: ChipLedSize[] = [ | ||
| { | ||
| imperial: "0201", | ||
| p_mm: 0.66, | ||
| pw_mm: 0.46, | ||
| ph_mm: 0.4, | ||
| body_w_mm: 0.6, | ||
| body_h_mm: 0.3, | ||
| }, | ||
| { | ||
| imperial: "0402", | ||
| p_mm: 1.0, | ||
| pw_mm: 0.5, | ||
| ph_mm: 0.5, | ||
| body_w_mm: 1.0, | ||
| body_h_mm: 0.5, | ||
| }, | ||
| { | ||
| imperial: "0603", | ||
| p_mm: 1.6, | ||
| pw_mm: 0.8, | ||
| ph_mm: 0.95, | ||
| body_w_mm: 1.6, | ||
| body_h_mm: 0.8, | ||
| }, | ||
| { | ||
| imperial: "0805", | ||
| p_mm: 1.9, | ||
| pw_mm: 1.0, | ||
| ph_mm: 1.45, | ||
| body_w_mm: 2.0, | ||
| body_h_mm: 1.25, | ||
| }, | ||
| { | ||
| imperial: "1206", | ||
| p_mm: 3.0, | ||
| pw_mm: 1.0, | ||
| ph_mm: 1.75, | ||
| body_w_mm: 3.2, | ||
| body_h_mm: 1.6, | ||
| }, | ||
| ] | ||
|
|
||
| const imperialMap = Object.fromEntries(chipLedSizes.map((s) => [s.imperial, s])) | ||
|
|
||
| export const chipled_def = base_def.extend({ | ||
| fn: z.string(), | ||
| imperial: z.string().optional(), | ||
| p: z.number().optional(), | ||
| pw: z.number().optional(), | ||
| ph: z.number().optional(), | ||
| w: z.number().optional(), | ||
| h: z.number().optional(), | ||
| string: z.string().optional(), | ||
| }) | ||
|
|
||
| export const chipled = ( | ||
| raw_params: z.input<typeof chipled_def>, | ||
| ): { circuitJson: AnyCircuitElement[]; parameters: any } => { | ||
| const parameters = chipled_def.parse(raw_params) | ||
|
|
||
| // Determine size from imperial code in the string (e.g. "chipled0603") | ||
| let sz: ChipLedSize | undefined | ||
| if (parameters.imperial) { | ||
| sz = imperialMap[parameters.imperial] | ||
| } | ||
| if (!sz && parameters.string) { | ||
| const match = parameters.string.match(/chipled(\d{4,5})/i) | ||
| if (match) { | ||
| sz = imperialMap[match[1]!] | ||
| } | ||
| } | ||
|
|
||
| // Fallback: try to match imperial from passive footprint sizes | ||
| if (!sz && parameters.imperial) { | ||
| const passive = footprintSizes.find( | ||
| (s) => s.imperial === parameters.imperial, | ||
| ) | ||
| if (passive) { | ||
| sz = { | ||
| imperial: passive.imperial, | ||
| p_mm: passive.p_mm_min, | ||
| pw_mm: passive.pw_mm_min, | ||
| ph_mm: passive.ph_mm_min, | ||
| body_w_mm: passive.w_mm_min, | ||
| body_h_mm: passive.h_mm_min, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Use defaults (0603) if still not found | ||
| if (!sz) { | ||
| sz = imperialMap["0603"]! | ||
| } | ||
|
|
||
| const p = parameters.p ?? sz.p_mm | ||
| const pw = parameters.pw ?? sz.pw_mm | ||
| const ph = parameters.ph ?? sz.ph_mm | ||
| const bodyW = parameters.w ?? sz.body_w_mm | ||
| const bodyH = parameters.h ?? sz.body_h_mm | ||
|
|
||
| // Silkscreen outline dimensions | ||
| const silkW = Math.max(bodyW, p - pw) * 0.5 | ||
| const silkH = Math.max(bodyH, ph) * 0.5 + 0.1 | ||
|
|
||
| // Silkscreen body outline (box, leaving gaps at pad sides) | ||
| const gapHalf = pw / 2 + 0.1 | ||
| const silkscreenBody: PcbSilkscreenPath = { | ||
| type: "pcb_silkscreen_path", | ||
| layer: "top", | ||
| pcb_component_id: "", | ||
| pcb_silkscreen_path_id: "silkscreen_body", | ||
| stroke_width: 0.05, | ||
| route: [ | ||
| { x: -silkW, y: -silkH }, | ||
| { x: -silkW, y: silkH }, | ||
| { x: silkW, y: silkH }, | ||
| { x: silkW, y: -silkH }, | ||
| { x: -silkW, y: -silkH }, | ||
| ], | ||
| } | ||
|
|
||
| // Cathode polarity bar: vertical line on the cathode (pin 1 / left) side | ||
| // Positioned just inside the body outline on the cathode edge | ||
| const barX = -bodyW * 0.15 | ||
| const polarityBar: PcbSilkscreenPath = { | ||
| type: "pcb_silkscreen_path", | ||
| layer: "top", | ||
| pcb_component_id: "", | ||
| pcb_silkscreen_path_id: "polarity_bar", | ||
| stroke_width: 0.1, | ||
| route: [ | ||
| { x: barX, y: -silkH }, | ||
| { x: barX, y: silkH }, | ||
| ], | ||
| } | ||
|
|
||
| // Pads: pin 1 = K (cathode) on the left, pin 2 = A (anode) on the right | ||
| const pad1 = rectpad(["1", "K"], -p / 2, 0, pw, ph) | ||
| const pad2 = rectpad(["2", "A"], p / 2, 0, pw, ph) | ||
|
|
||
| const silkscreenRefText: SilkscreenRef = silkscreenRef(0, silkH + 0.4, 0.3) | ||
|
|
||
| const courtyardPadding = 0.25 | ||
| const crtMinX = -(p / 2 + pw / 2) - courtyardPadding | ||
| const crtMaxX = p / 2 + pw / 2 + courtyardPadding | ||
| const crtMinY = -silkH - courtyardPadding | ||
| const crtMaxY = silkH + courtyardPadding | ||
| const courtyard: PcbCourtyardRect = { | ||
| type: "pcb_courtyard_rect", | ||
| pcb_courtyard_rect_id: "", | ||
| pcb_component_id: "", | ||
| center: { x: (crtMinX + crtMaxX) / 2, y: (crtMinY + crtMaxY) / 2 }, | ||
| width: crtMaxX - crtMinX, | ||
| height: crtMaxY - crtMinY, | ||
| layer: "top", | ||
| } | ||
|
|
||
| return { | ||
| circuitJson: [ | ||
| pad1, | ||
| pad2, | ||
| silkscreenBody, | ||
| polarityBar, | ||
| silkscreenRefText, | ||
| courtyard, | ||
| ], | ||
| parameters, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { test, expect } from "bun:test" | ||
| import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" | ||
| import { fp } from "../src/footprinter" | ||
|
|
||
| test("chipled0603", () => { | ||
| const circuitJson = fp.string("chipled0603").circuitJson() | ||
| const svgContent = convertCircuitJsonToPcbSvg(circuitJson) | ||
| expect(svgContent).toMatchSvgSnapshot(import.meta.path, "chipled0603") | ||
| }) | ||
|
|
||
| test("chipled0402", () => { | ||
| const circuitJson = fp.string("chipled0402").circuitJson() | ||
| const svgContent = convertCircuitJsonToPcbSvg(circuitJson) | ||
| expect(svgContent).toMatchSvgSnapshot(import.meta.path, "chipled0402") | ||
| }) | ||
|
|
||
| test("chipled0805", () => { | ||
| const circuitJson = fp.string("chipled0805").circuitJson() | ||
| const svgContent = convertCircuitJsonToPcbSvg(circuitJson) | ||
| expect(svgContent).toMatchSvgSnapshot(import.meta.path, "chipled0805") | ||
| }) | ||
|
|
||
| test("chipled1206", () => { | ||
| const circuitJson = fp.string("chipled1206").circuitJson() | ||
| const svgContent = convertCircuitJsonToPcbSvg(circuitJson) | ||
| expect(svgContent).toMatchSvgSnapshot(import.meta.path, "chipled1206") | ||
| }) | ||
|
Comment on lines
+5
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test file contains 4 test() calls (lines 5, 11, 17, and 23), but the rule states that a *.test.ts file may have AT MOST one test(...), after that the user should split into multiple, numbered files. This file should be split into multiple files like chipled1.test.ts, chipled2.test.ts, chipled3.test.ts, and chipled4.test.ts, with each file containing only one test() call. Spotted by Graphite (based on custom rule: Custom rule) |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The silkscreen width calculation causes overlap with the pads. The silkscreen extends to
±silkWbut pads start at±(p/2 - pw/2). For 0603: silkW=0.8mm but pad inner edge is at 0.4mm, causing overlap.Fix by constraining silkW to not exceed the pad inner edge:
The
gapHalfvariable on line 134 appears to be calculated but never used for this purpose.Spotted by Graphite

Is this helpful? React 👍 or 👎 to let us know.