molecule
Evan — Adom Molecule LED Nameplate
Public Made by Adomby adom
Evan's name in 66 green 0603 LEDs on a 5x7 dot-matrix grid, USB-C powered off the east edge. A real Adom molecule: four corner machine pins, 96x44 mm absolute, every part from the Adom Basic Parts catalog.
main
John Lauer
README.html with embedded 3D walkthrough viewer, full BOM and schematic, source bundle, build outputs
23c1d5e
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
/**
* TYPE-C-31-M-12 USB-C receptacle (JLCPCB C165948).
*
* ── Why the geometry below is what it is ──────────────────────────────────
*
* Every constant here was measured off the actual model
* (modelcdn easyeda C165948, 7872 verts) rather than assumed, because the
* assumed version renders wrong in a way that still passes lint.
*
* Raw model bounding box, in model units (mm):
* x -4.470 .. +4.470 8.94 mm wide (across the connector)
* y -5.300 .. +2.600 7.90 mm deep (mouth to back)
* z -0.850 .. +3.250 legs below board, shell 3.25 mm proud
*
* The `rotationOffset.z: 180` that the registry packages ship is NOT
* decoration. It is what aligns the model with this footprint. Reconstructing
* the registry placement shows the model's raw −5.3 face is the BACK (the end
* the SMT pads protrude from) and the raw +2.6 face is the MOUTH; the 180°
* flip puts the mouth at −Y in the footprint's frame. The footprint agrees:
* its mechanical holes sit at y = −1.04, −1.27 and −5.22 relative to the pad
* row, i.e. on the −Y side, which is only physically possible if the shell,
* and therefore the mouth, is also on the −Y side. Anchor legs live inside
* the shell, never behind it.
*
* Drop the 180° and keep the native hole positions and you get a board where
* the body floats forward of its own pads while the anchors trail out the
* back. `adom-tsci lint` does not catch this: it derives the mouth vector from
* `cad.rotation.z` using the same +Y assumption, so a 180° error cancels out
* of the check and reports a healthy `body_fwd`. Look at the 3D view.
*
* ── Placement ─────────────────────────────────────────────────────────────
*
* Relative to the pad row, the shell spans +0.06 (back, overlapping the pads
* slightly) to −7.84 (mouth), and the forward anchor pair sits at −5.22, i.e.
* 2.6 mm behind the mouth. That last number is the real constraint on how far
* the connector can hang off the board: overhang the mouth by the ~2.5 mm
* quoted for a pads-only footprint and the forward anchors land ON the board
* edge. With the anchors included the honest maximum is well under 1 mm:
* flush-to-slightly-proud, which is all the mechanical spec asks for anyway.
*/
const USB_C_PN = "C165948"
const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"
/**
* ── Re-derived 2026-08-20, measured off the placed GLB ────────────────────
*
* The upstream constant (BODY_FORWARD_MAG = 2.5034) leaves the shell floating
* 2.09 mm forward of its own pad row and hanging 2.5 mm off the board edge.
* The cause is that tscircuit anchors `positionOffset` at
* `pcb_component.center`, the bbox of pads AND holes, not at the pad row.
* All of this footprint's holes sit on the mouth side, so that centre is
* 2.7366 mm forward of the pads and the offset inherits the error.
*
* Footprint bbox along the mouth axis (pad row at 0, mouth at −y):
* pads 1.3 mm tall → [−0.65, +0.65]
* anchors −1.0434 ± 1.0 → [−2.0434, −0.0434]
* −5.2232 ± 0.9 → [−6.1232, −4.3232]
* supports −1.2685 ± 0.375 → [−1.6435, −0.8935]
* bbox [−6.1232, +0.65] → centre −2.7366
*
* Model, measured from the J1 node of a built `3d.glb` (the thing that is
* actually rendered, so it cannot disagree with what you see):
* origin → back face 3.15 mm against the mouth
* origin → mouth face 4.75 mm with the mouth (7.90 mm total, matches
* the 7.9 mm shell depth)
*
* Verify with scripts/check-usbc.mjs after any change here: it reads the GLB
* and asserts the shell's back face lands on the pad row and its mouth lands
* just proud of the board edge.
*/
const FOOTPRINT_CENTER_FROM_PADS = 2.7366
const MODEL_ORIGIN_TO_BACK = 3.15
const MODEL_ORIGIN_TO_MOUTH = 4.75
/** Offset that puts the shell's back face exactly on the pad row. */
const BODY_FORWARD_MAG = MODEL_ORIGIN_TO_BACK - FOOTPRINT_CENTER_FROM_PADS
/** Shell front face, measured forward from the pad row. */
export const USB_C_MOUTH_FROM_PADS = MODEL_ORIGIN_TO_BACK + MODEL_ORIGIN_TO_MOUTH
/** Forward anchor centre, measured forward from the pad row. */
export const USB_C_FRONT_ANCHOR_FROM_PADS = 5.2232
/** Half-length of the forward anchor pill along the mouth axis. */
export const USB_C_FRONT_ANCHOR_HALF = 0.9
/**
* Where to put `pcbX`/`pcbY` (the pad row) so the mouth ends up `proud` mm
* past a board edge that sits `edge` mm from the origin.
*/
export const usbCPadRowForEdge = (edge: number, proud = 0.35) =>
edge + proud - USB_C_MOUTH_FROM_PADS
const pinLabels = {
pin1: ["GND1", "A1"],
pin2: ["GND2", "B12"],
pin3: ["VBUS1", "A4"],
pin4: ["VBUS2", "B9"],
pin5: ["SBU2", "B8"],
pin6: ["CC1", "A5"],
pin7: ["DM2", "B7"],
pin8: ["DP1", "A6"],
pin9: ["DM1", "A7"],
pin10: ["DP2", "B6"],
pin11: ["SBU1", "A8"],
pin12: ["CC2", "B5"],
pin13: ["VBUS1", "A9"],
pin14: ["VBUS2", "B4"],
pin15: ["GND1", "A12"],
pin16: ["GND2", "B1"],
} as const
// 16 SMT pads, 0.5 mm pitch, pad row pinned to y = 0.
const PADS: Array<{ port: string; x: number }> = [
{ port: "A1", x: -3.35 },
{ port: "B12", x: -3.05 },
{ port: "A4", x: -2.55 },
{ port: "B9", x: -2.25 },
{ port: "B8", x: -1.75 },
{ port: "A5", x: -1.25 },
{ port: "B7", x: -0.75 },
{ port: "A6", x: -0.25 },
{ port: "A7", x: 0.25 },
{ port: "B6", x: 0.75 },
{ port: "A8", x: 1.25 },
{ port: "B5", x: 1.75 },
{ port: "B4", x: 2.25 },
{ port: "A9", x: 2.55 },
{ port: "B1", x: 3.05 },
{ port: "A12", x: 3.35 },
]
// Mechanical holes. y is measured from the pad row, negative = toward the
// mouth, so all of these sit under the shell.
const PLATED_ANCHORS = [
{ port: "alt_0", x: -4.3251, y: -1.0434, outerH: 2.0, innerH: 1.6 },
{ port: "alt_1", x: 4.3251, y: -1.0434, outerH: 2.0, innerH: 1.6 },
{ port: "alt_3", x: -4.3251, y: -5.2232, outerH: 1.8, innerH: 1.4 },
{ port: "alt_2", x: 4.3251, y: -5.2232, outerH: 1.8, innerH: 1.4 },
]
const SUPPORT_HOLES = [
{ x: -2.8999, y: -1.2685 },
{ x: 2.8999, y: -1.2685 },
]
export const UsbCReceptacle = ({
name,
pcbX,
pcbY,
pcbRotation = 0,
schX,
schY,
schRotation,
}: {
name: string
/** Position of the PAD ROW, not the shell. Use `usbCPadRowForEdge`. */
pcbX: number
pcbY: number
/** 0 fires the mouth south, 90 east, 180 north, 270 west. */
pcbRotation?: 0 | 90 | 180 | 270
schX?: number
schY?: number
schRotation?: number
}) => {
// Mouth is local −Y. Rotating (0,−1) by pcbRotation CCW gives the world
// mouth vector; positionOffset is expressed along that same vector so the
// body tracks the footprint through every rotation instead of drifting
// (tscircuit applies positionOffset in world coords, not local).
const rad = (pcbRotation * Math.PI) / 180
const mouthX = Math.sin(rad)
const mouthY = -Math.cos(rad)
return (
<chip
name={name}
pcbX={pcbX}
pcbY={pcbY}
pcbRotation={pcbRotation}
schX={schX}
schY={schY}
schRotation={schRotation}
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}`,
// 180° is load-bearing: it aligns the model's mouth with the −Y mouth
// this footprint's hole positions imply. Do not "simplify" it away.
rotationOffset: { x: 0, y: 0, z: 180 },
positionOffset: {
x: BODY_FORWARD_MAG * mouthX,
y: BODY_FORWARD_MAG * mouthY,
z: 0,
},
}}
footprint={
<footprint>
{PADS.map((p) => (
<smtpad
key={p.port}
portHints={[p.port]}
shape="rect"
width="0.3mm"
height="1.3mm"
pcbX={`${p.x}mm`}
pcbY="0mm"
/>
))}
{PLATED_ANCHORS.map((h) => (
<platedhole
key={h.port}
portHints={[h.port]}
shape="pill"
pcbX={`${h.x}mm`}
pcbY={`${h.y}mm`}
outerWidth="1.2mm"
outerHeight={`${h.outerH}mm`}
innerWidth="0.8mm"
innerHeight={`${h.innerH}mm`}
/>
))}
{SUPPORT_HOLES.map((h, i) => (
<hole key={i} pcbX={`${h.x}mm`} pcbY={`${h.y}mm`} diameter="0.75mm" />
))}
</footprint>
}
/>
)
}