← Commit history

Migrate asset from Helium wiki

Kyle Bergstedt ·ad1fc68fd8 ·4mo ago ·parent 0d6e3bb
1 file changed +341
adom-usbc.mdadded+341
@@ -0,0 +1,341 @@+# adom-usbc — making USB-C receptacles actually work++**Read this before touching a USB-C receptacle on an Adom tscircuit+board.** Written because Claude got USB-C wrong ~20 times before we+wrote the rules down. If you skip this skill and try to wing it, you+will repeat every one of those failures.++## THE CORE INVARIANT — DO NOT VIOLATE++**Footprint (pads + holes) and cadModel (3D body) are tied together.+Whatever rotation you apply to one MUST be applied identically to the+other. Same rotation AND same translation.** They are one physical+object. Treating them as independently rotatable is the root cause+of every USB-C alignment bug we've hit.++Ways to satisfy this invariant:++1. **Bundle them in one component** with a single `pcbRotation` prop+   that applies to BOTH the `<chip>`'s footprint children AND the+   cadModel's `positionOffset`. The wrapper in §"The workaround"+   below does this — it computes `positionOffset` as `rotate(native_offset,+   pcbRotation)` in userland so the body rotates with the footprint.+2. **Compute body placement in the SAME local frame as the footprint.**+   Never express body offset in world coords while the footprint is+   in local coords. If you set `positionOffset` directly without+   rotating it by `pcbRotation`, the body will drift off its pads on+   every non-zero rotation.++The common violation: "I'll rotate the chip 270° for east edge, and+I'll add 2.5 mm positionOffset to shift the body forward." The first+rotation applies to pads/holes (tscircuit auto-rotates them in the+footprint local frame). The second shift applies to the body (in+world frame, no rotation). They decouple. The body ends up on the+wrong side of the pads, and the anchor holes end up OUTSIDE the body+footprint. I rebuilt this ~15 times before realising this was the+bug, not a tuning issue.++**Test for the invariant**: the distance between the body centroid+and each anchor hole must be the same at `pcbRotation=0` as it is at+`pcbRotation=90/180/270`. If that distance changes with rotation, the+invariant is broken, the build is wrong, and you must NOT ship the+board.++## The fundamental problem++`@tsci/seveibar.smd-usb-c` and `@tsci/ArnavK-09.smd-usb-c` both+hardcode a `cadModel` with:++```js+cadModel: {+  objUrl: "…easyeda_models/download?…pn=C165948",+  rotationOffset: { x: 0, y: 0, z: 180 },+  positionOffset: { x: 0, y: -2.5, z: 0 },+}+```++This works at the default `pcbRotation=0` (mouth facing south) because+`(0, -2.5)` puts the body 2.5 mm south of the pad centroid, which is+where the receptacle's body sits relative to its back-row of SMT pads.++**It breaks at every other rotation.** Two reasons:++1. **`positionOffset` is applied in world coords, not rotated with+   `pcbRotation`.** So at `pcbRotation=270` (pads rotated to the east),+   the body stays 2.5 mm south instead of moving 2.5 mm *east* with+   the pads. The body drifts off the pads and the mouth points into+   the board.+2. **The wrapper's `{...props, cadModel: HARDCODED}` spread order+   silently discards any `cadModel` override passed by the caller.**+   So you can't fix bug #1 by passing `cadModel` on `<SmdUsbC>`.++Filed upstream as `TSCIRCUIT_FEATURE_REQUESTS.md §6`.++## The workaround: a local `UsbCReceptacle.tsx`++Ship a local wrapper in your project's `lib/` that builds a raw+`<chip>` with an explicit footprint (copied from ArnavK-09's JSX) and+your own `cadModel` that rotates `positionOffset` with `pcbRotation`.+This survives `bun install`, gives you full control, and — critically+— you can unit-test it with the lint below without waiting on+upstream fixes.++### The template (copy this verbatim)++```tsx+// lib/UsbCReceptacle.tsx+const USB_C_PN = "C165948"+const USB_C_UUID = "2a4bc2358b36497d9ab2a66ab6419ba3"++// Body-to-pad offset in the mouth direction for TYPE-C-31-M-12.+// Measured: the SMT pad row sits at the BACK of the receptacle, and+// the body centroid is 2.5 mm forward of that. Centering body on pads+// (positionOffset 0) makes the back half of the body extend BEHIND+// the pads into the board — wrong geometry, even if numerics look+// "clean".+const BODY_FORWARD_MAG = 2.5++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, pitch 0.5 mm, centered at y=0 so bounds.center = pad+// centroid (no holes included — see §Why we drop the holes).+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 },+]++export const UsbCReceptacle = ({+  name, pcbX, pcbY, pcbRotation = 0,+}: {+  name: string; pcbX: number; pcbY: number;+  pcbRotation?: 0 | 90 | 180 | 270+}) => {+  // Native CAD has mouth at local +Y. cad.rotation.z = pcbRotation+  // + rotationOffset.z (tscircuit adds them, CCW). We set+  // rotationOffset.z=0 so cad_z = pcbRotation. Then the mouth direction+  // in world frame is rotate((0,+1), pcbRotation CCW) =+  // (-sin pcbRotation, cos pcbRotation).+  const rad = (pcbRotation * Math.PI) / 180+  const mouthX = -Math.sin(rad)+  const mouthY =  Math.cos(rad)+  const posX = BODY_FORWARD_MAG * mouthX+  const posY = BODY_FORWARD_MAG * mouthY++  return (+    <chip+      name={name}+      pcbX={pcbX} pcbY={pcbY} pcbRotation={pcbRotation}+      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}`,+        rotationOffset: { x: 0, y: 0, z: 0 },+        positionOffset: { x: posX, y: posY, 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" />+          ))}+        </footprint>+      }+    />+  )+}+```++## Per-edge placement recipe++For a board of width `W` centered at x=0 (so edges at ±W/2):++| Edge you want mouth to face | `pcbRotation` | `pcbX` | `pcbY` |+|---|---|---|---|+| South (bottom) | 0   | 0 or offset along x  | −(H/2) + 4 |+| East (right)   | 270 | +(W/2) − 4           | 0 or offset along y |+| North (top)    | 180 | 0 or offset along x  | +(H/2) − 4 |+| West (left)    | 90  | −(W/2) + 4           | 0 or offset along y |++`pcbX / pcbY` is the **chip origin** = **pad row centroid** position.+For a 64×32 board with USB-C on east edge: `pcbX=28, pcbY=0,+pcbRotation=270`. Pads at x=28 (inside the board by 4 mm), body at+x=28+2.5=30.5, mouth cantilevers to about x=34.5 (2.5 mm past edge).++### **Corollary: no SMT pad may extend past the board edge.**++The **body / mouth / mechanical shell** can and should cantilever+past the edge. SMT **pads** must not. If any copper pad (VBUS1/2,+GND1/2, DP1/2, DM1/2, CC1, CC2, SBU1/2) ends up at `|pcbX| > W/2`,+tscircuit grows the GLB board mesh asymmetrically to include the+overhang — and because the texture art is drawn with `pcbX=0` at+the canvas centre, the whole silkscreen / copper / annular-ring+layer renders shifted by `asymmetry/2` relative to the 3D mesh.+Every via drill appears off-centre in its annular ring, every+passive body appears shifted from its pad. The PCB itself is fine,+but the 3D view looks broken.++Real failure: a 72×32 board with the USB-C receptacle at `pcbX=35`.+That put the east-most SMT pads at `pcbX≈37.2`, growing the mesh+to `[-38.174, +37.2]` (centre `-0.487`) and shifting every copper+feature ~0.5 mm in the render. Fix: moved the receptacle to+`pcbX=33` (= `W/2 − 4 + overhang_0`). Mesh became `[-37.2, +37.2]`,+shift gone. **Use `pcbX = W/2 − 4` for the east edge; do not improvise.**++## Mandatory verification lint++**Run this after every build with a USB-C receptacle.** Not+optional. The only way we stopped getting USB-C wrong was by+gating on these two numeric checks:++```js+// scripts/check-usbc.mjs+import j from "./dist/lib/index/circuit.json" with { type: "json" }++for (const src of j.filter(x => x.type === "source_component")) {+  // Heuristic: USB-C receptacle = any chip with jlcpcb matching a+  // known USB-C family. Extend as new families are supported.+  const pn = src.supplier_part_numbers?.jlcpcb?.[0]+  if (!pn || !/^C(165948|2760486|283540|840342)/.test(pn)) continue++  const pc = j.find(x => x.type === "pcb_component"+                      && x.source_component_id === src.source_component_id)+  const pads = j.filter(x => x.type === "pcb_smtpad"+                          && x.pcb_component_id === pc.pcb_component_id)+  const cad = j.find(x => x.type === "cad_component"+                        && x.pcb_component_id === pc.pcb_component_id)+  const board = j.find(x => x.type === "pcb_board")++  const padCx = pads.reduce((s, p) => s + p.x, 0) / pads.length+  const padCy = pads.reduce((s, p) => s + p.y, 0) / pads.length+  const rot   = cad.rotation.z * Math.PI / 180+  const mX    = -Math.sin(rot), mY = Math.cos(rot)++  // Check 1: body forward of pads by ~BODY_FORWARD_MAG in mouth dir+  const fwd = (cad.position.x - padCx) * mX + (cad.position.y - padCy) * mY+  if (fwd < 1.5 || fwd > 3.5) {+    throw new Error(`${src.name}: body_forward=${fwd.toFixed(2)} mm; expected ~2.5.+      Body is ${fwd < 0 ? "BEHIND" : "too close to"} pads — geometry wrong.`)+  }++  // Check 2: mouth ray exits the board outline+  const ray = { x: cad.position.x + 12 * mX, y: cad.position.y + 12 * mY }+  const bx = board.center?.x ?? 0, by = board.center?.y ?? 0+  const inside = ray.x >= bx - board.width/2 && ray.x <= bx + board.width/2+              && ray.y >= by - board.height/2 && ray.y <= by + board.height/2+  if (inside) {+    throw new Error(`${src.name}: mouth points INTO the board at ${ray.x.toFixed(1)},${ray.y.toFixed(1)}.+      Cable would collide with FR4. Rotate receptacle 180° (toggle rotationOffset.z 0↔180).`)+  }++  console.log(`✓ ${src.name}: body_fwd=${fwd.toFixed(2)}mm, mouth dir (${mX.toFixed(2)},${mY.toFixed(2)}) exits board at (${ray.x.toFixed(1)},${ray.y.toFixed(1)})`)+}+```++**Wire it into your build loop:**++```bash+bunx tsci build lib/index.tsx --glbs --svgs && adom-tsci lint+```++`adom-tsci lint` does exactly the checks above (for every JLC USB-C+part in its known list) plus catches ghost chips and autorouter+silent-failures. Same exit-code gating. Prefer it over the inline+`node -e` above — it's shipped with the tool and maintained+alongside the lint rules.++Or if you only have `node` and not `adom-tsci lint`:++## Visual proof-of-placement (the toggle-and-shotlog ritual)++**Numeric lint can be gamed.** A body can satisfy `forward_dot >= 1.5`+yet still be visually misaligned with the pad geometry if a footprint+dimension is wrong. After every USB-C placement change, do this+three-step visual proof:++```bash+# 1. Top view with receptacle visible+adom-tsci view top --port 8853+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C placed on east edge, body visible cantilevered off the board" -s pup_screenshot <path>++# 2. Hide the receptacle to see the pads underneath+adom-tsci toggle-component J1 --port 8853 --hide+adom-desktop browser_screenshot '{"sessionId":"<project>","maxWidth":1800}'+shotlog inject -c usbc-validate -d "USB-C hidden; pads underneath should be flush with board east edge, all 16 pads on copper" -s pup_screenshot <path>++# 3. Un-hide+adom-tsci toggle-component J1 --port 8853 --show+```++The user reads the two screenshots side-by-side and confirms:+- Body cantilevers off the correct edge (mouth away from board)+- The pads sit inside the board (none hanging off)+- Pad row aligns with where the body's back edge is++**If you don't post both screenshots to shotlog, the user can't+verify, and you can't claim the placement works.** "The lint passes"+is necessary but not sufficient.++## Include ALL mounting holes — do NOT drop them to simplify the math++**The rule:** the TYPE-C-31-M-12 footprint must include all 4 plated+corner anchor holes + 2 drill-only support holes. Without the anchors+the connector is held by 16 tiny SMT joints only, which tear off the+board under cable insert/extract stress in well under 100 cycles. A+USB-C port that rips loose after a week is a bug, not a trade-off.++**The compensation:** tscircuit computes `pcb_component.center` as+the bbox of pads + holes. For the TYPE-C-31-M-12 layout (pads at+local y=0, holes at y ∈ {-1.04, -1.27, -5.22}), the bbox center sits+2.61 mm south of the pad centroid. To keep the cad body 2.5 mm+forward of the pad row, bump `BODY_FORWARD_MAG` from 2.5 to 2.5 ++2.61 = **5.11** in the wrapper template.++Formula for any directional connector with asymmetric holes:++```text+BODY_FORWARD_MAG = body_over_pads + |bbox_center_y − pad_row_y|+```++Measure the bbox shift by building once with holes included, reading+`pcb_component.center` from `circuit.json`, and subtracting your+known pad row y.++Filed upstream as TSCIRCUIT_FEATURE_REQUESTS §6.1: `cadModel` should+accept `boundsSource: "pads" | "all"` so we don't have to compensate+in userland. Until then, the compensation above is load-bearing.++## Failure modes → diagnosis table++| Symptom | Cause | Fix |+|---|---|---|+| Body visible but pads hanging off the board edge | `pcbX/Y` too close to the edge; pads should be INSIDE copper | Move `pcbX` inward by ~4 mm |+| Mouth points into the board (butt cantilevers off edge) | `rotationOffset.z` is wrong by 180° | Flip `rotationOffset.z` (0 ↔ 180) |+| Body drifts diagonally off the pads at non-zero rotation | You used upstream `SmdUsbC` without the rotation-compensation | Switch to the local `UsbCReceptacle.tsx` wrapper above |+| Body centered on pads, but half the body extends into the board | `positionOffset` is (0,0); forgot `BODY_FORWARD_MAG * mouth_dir` | Add the forward-offset math |+| USB-C missing from Components HUD in adom-tsci | refdes doesn't match `/^J[\d_]/i` OR no cad_component emitted | Rename to `J1`/`J_1`; check `circuit.json` for `cad_component` row |+| `pcb_autorouting_error: Failed to solve 1 nodes` after adding USB-C | USB-C pads near high-density chip pin row | Move USB-C further from adjacent chips, or grow board |++## Upstream feature requests++See `adom-tsci/TSCIRCUIT_FEATURE_REQUESTS.md §6` for:+- cadModel.positionOffset should co-rotate with pcbRotation+- @tsci wrappers should use `{ cadModel: props.cadModel ?? DEFAULT, ...props }` so caller overrides work+- bounds.center should be opt-in pads-only for cadModel placement