molecule
Steven's Nameplate: three revisions
Public Made by Adomby adom
A USB-C desk nameplate for Steven Moss (Astera Institute), built three ways and published as open experiments: a 3x5 LED board, a 5x7 LED board, and a laser-ablated silkscreen plate. Source, fab files, negative results, reproduce steps.
main
John Lauer
Three revisions with per-experiment write-ups, inline 3D viewers, sources, fab files, open-science scorecard
091a7c9
12d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
/**
* USB-C receptacle (TYPE-C-31-M-12 / JLCPCB C165948).
*
* ─────────────── THE CORE INVARIANT ───────────────
* Footprint (pads + holes) and cadModel (3D body) are TIED TOGETHER.
* Whatever rotation is applied to one MUST be applied identically to
* the other. Earlier iterations violated this by (a) shifting pad
* positions in the footprint (Y_SHIFT) while leaving the cadModel
* offset in a different frame, and (b) letting tscircuit apply
* positionOffset in WORLD coords while pads rotated in LOCAL. Result:
* at pcbRotation≠0 the 3D body drifted away from its own holes.
*
* Fix: keep footprint at native ArnavK-09 positions AND compute
* positionOffset by rotating the native (0, -2.5) vector by pcbRotation
* in userland. Both footprint and body then follow the SAME rotation.
*
* ─────────────── NATIVE LAYOUT ───────────────
* pads at local (x, +2.449) for 16 pads — this is the BACK edge of
* the receptacle (solder-tails come out
* the back of the plastic shell)
* drill holes at (±2.9, +1.18)
* plated anchor holes at (±4.325, +1.41) and (±4.325, -2.774)
* — 4 corners of the receptacle body
* body centroid at local (0, -2.5) in native (from SmdUsbC hardcode)
* mouth at local -Y (south) in native
*
* This layout has body_centroid south of pads by 4.95 mm — that's the
* distance from the pad row (back of receptacle) through the body to
* the body's midpoint. Rotated together, this 4.95 mm distance is
* preserved at every pcbRotation so holes ALWAYS stay inside the body.
*/
const USB_C_PN = "C165948"
const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"
// Native positionOffset from ArnavK-09 / seveibar.smd-usb-c (pcbRotation=0):
// body centroid is 2.5 mm south of chip origin. Rotated by pcbRotation
// in userland below so the body rotates with the footprint.
const NATIVE_POS_OFFSET = { x: 0, y: -2.5 }
// 12 physical pads (see PADS below): the four same-net pairs that USB-C
// doubles for flip symmetry are single wider tabs, exactly as they are on
// the real TYPE-C-31-M-12 land pattern. Each merged tab is ONE pin carrying
// both of its native contact designators.
//
// The GND1/GND2/VBUS1/VBUS2 aliases are kept, but now mean LEFT/RIGHT tab
// rather than the A-row/B-row contact. That is what makes them unambiguous:
// one alias -> exactly one pad. Naming them per-row instead (the native
// scheme) would put VBUS1 on both tabs and VBUS2 on both tabs, and the
// router would emit a degenerate pad-to-itself trace.
const pinLabels = {
pin1: ["GND1", "A1", "B12"], // left GND tab (merged)
pin2: ["VBUS1", "A4", "B9"], // left VBUS tab (merged)
pin3: ["SBU2", "B8"],
pin4: ["CC1", "A5"],
pin5: ["DM2", "B7"],
pin6: ["DP1", "A6"],
pin7: ["DM1", "A7"],
pin8: ["DP2", "B6"],
pin9: ["SBU1", "A8"],
pin10: ["CC2", "B5"],
pin11: ["VBUS2", "B4", "A9"], // right VBUS tab (merged)
pin12: ["GND2", "B1", "A12"], // right GND tab (merged)
} as const
// ArnavK-09 native positions — DO NOT SHIFT in the mouth (Y) direction.
// Shifting Y decouples the footprint from the cadModel, which was the bug.
//
// X-direction merge (safe, and required to pass DRC): the four native
// pad PAIRS that sit on 0.3 mm pitch — (A1,B12) and (B1,A12) are both
// GND, (A4,B9) and (B4,A9) are both VBUS — are 0.3 mm wide at 0.3 mm
// spacing, i.e. exactly 0 mm clearance. `adom-tsci lint` fails that as
// pcb_pad_pad_clearance_error (min 0.1 mm), and the doubled labels also
// make `J1.GND1` an ambiguous multi-pad reference. Because each pair is
// the SAME NET (USB-C doubles those pins for flip symmetry), the correct
// fix is to merge each pair into one 0.6 mm pad carrying both port hints.
// This keeps the pad centroid at x=0 (so the body/rotation invariant is
// untouched) and restores 0.2 mm clearance everywhere.
const PAD_Y_LOCAL = 2.449 // all pads at this y
const PADS: Array<{ ports: string[]; x: number; w: number }> = [
{ ports: ["A1", "B12"], x: -3.2, w: 0.6 }, // GND (merged pair)
{ ports: ["A4", "B9"], x: -2.4, w: 0.6 }, // VBUS (merged pair)
{ ports: ["B8"], x: -1.75, w: 0.3 },
{ ports: ["A5"], x: -1.25, w: 0.3 },
{ ports: ["B7"], x: -0.75, w: 0.3 },
{ ports: ["A6"], x: -0.25, w: 0.3 },
{ ports: ["A7"], x: 0.25, w: 0.3 },
{ ports: ["B6"], x: 0.75, w: 0.3 },
{ ports: ["A8"], x: 1.25, w: 0.3 },
{ ports: ["B5"], x: 1.75, w: 0.3 },
{ ports: ["B4", "A9"], x: 2.4, w: 0.6 }, // VBUS (merged pair)
{ ports: ["B1", "A12"], x: 3.2, w: 0.6 }, // GND (merged pair)
]
/** Rotate a 2D vector by θ radians CCW (tscircuit's pcbRotation convention). */
const rotCCW = (x: number, y: number, θ: number) => ({
x: x * Math.cos(θ) - y * Math.sin(θ),
y: x * Math.sin(θ) + y * Math.cos(θ),
})
/**
* Mount convention — pass `mount` instead of raw `pcbRotation` so the
* edge-facing semantics are explicit. Native mouth = -Y (south) after
* the built-in rotationOffset.z = 180 flip, so:
* south → pcbRotation = 0
* east → pcbRotation = 90 (native -Y rotates CCW 90 to +X east... wait,
* CCW 90 of (0,-1) = (1, 0) = east? Let me re-derive:
* CCW θ: (x,y) → (x cos θ - y sin θ, x sin θ + y cos θ).
* For θ=90: (0,-1) → (0*0 - (-1)*1, 0*1 + (-1)*0) = (1, 0) = east ✓)
* north → pcbRotation = 180
* west → pcbRotation = 270
*/
type Mount = "south" | "east" | "north" | "west"
const MOUNT_TO_PCBROT: Record<Mount, 0 | 90 | 180 | 270> = {
south: 0, east: 90, north: 180, west: 270,
}
type Props = {
name: string
pcbX: number
pcbY: number
/** Where the mouth points. Equivalent: pass pcbRotation directly via `pcbRotationDeg`. */
mount?: Mount
pcbRotationDeg?: 0 | 90 | 180 | 270
/** Schematic placement, forwarded so the symbol does not pile at the origin (adom-usbc guide). */
schX?: number
schY?: number
}
export const UsbCReceptacle = ({
name, pcbX, pcbY, mount, pcbRotationDeg, schX, schY,
}: Props) => {
const pcbRot = pcbRotationDeg ?? (mount ? MOUNT_TO_PCBROT[mount] : 0)
const θ = (pcbRot * Math.PI) / 180
// Rotate the native positionOffset by pcbRotation so the body
// rotates with the footprint. This preserves the body-to-pad and
// body-to-hole distances at every rotation — the invariant the
// user (correctly) demanded.
const rotated = rotCCW(NATIVE_POS_OFFSET.x, NATIVE_POS_OFFSET.y, θ)
return (
<chip
name={name}
pcbX={pcbX}
pcbY={pcbY}
pcbRotation={pcbRot}
schX={schX}
schY={schY}
supplierPartNumbers={{ jlcpcb: [USB_C_PN] }}
manufacturerPartNumber="TYPE-C-31-M-12"
pinLabels={pinLabels as any}
cadModel={{
objUrl: `https://modelcdn.tscircuit.com/easyeda_models/download?uuid=${USB_C_UUID}&pn=${USB_C_PN}`,
// Native SmdUsbC rotationOffset — CAD body's native +Y mouth
// gets flipped to -Y so it matches the native footprint's
// mouth-south orientation.
rotationOffset: { x: 0, y: 0, z: 180 },
// Rotated version of the native (0, -2.5). This is what ties
// the body to the footprint.
positionOffset: { x: rotated.x, y: rotated.y, z: 0 },
}}
footprint={
<footprint>
{PADS.map(p => (
<smtpad key={p.ports.join("_")} portHints={p.ports}
shape="rect" width={`${p.w}mm`} height="1.3mm"
pcbX={`${p.x}mm`} pcbY={`${PAD_Y_LOCAL}mm`} />
))}
{/* 2 drill-only mechanical support holes, 1.18 mm south of chip origin */}
<hole pcbX="-2.9mm" pcbY="1.18mm" diameter="0.75mm" />
<hole pcbX="2.9mm" pcbY="1.18mm" diameter="0.75mm" />
{/* 4 plated anchor holes at the 4 body corners — 2 near pad row
(at y=+1.41) + 2 toward mouth (at y=-2.774). Rotate together
with the body, so they stay inside the body footprint. */}
<platedhole portHints={["alt_0"]} pcbX="-4.325mm" pcbY="1.41mm"
shape="pill" outerWidth="1.2mm" outerHeight="2.0mm"
innerWidth="0.8mm" innerHeight="1.6mm" height="1.6mm" />
<platedhole portHints={["alt_1"]} pcbX="4.325mm" pcbY="1.41mm"
shape="pill" outerWidth="1.2mm" outerHeight="2.0mm"
innerWidth="0.8mm" innerHeight="1.6mm" height="1.6mm" />
<platedhole portHints={["alt_2"]} pcbX="4.325mm" pcbY="-2.774mm"
shape="pill" outerWidth="1.2mm" outerHeight="1.8mm"
innerWidth="0.8mm" innerHeight="1.4mm" height="1.4mm" />
<platedhole portHints={["alt_3"]} pcbX="-4.325mm" pcbY="-2.774mm"
shape="pill" outerWidth="1.2mm" outerHeight="1.8mm"
innerWidth="0.8mm" innerHeight="1.4mm" height="1.4mm" />
</footprint>
}
/>
)
}