main
John Lauer Three revisions with per-experiment write-ups, inline 3D viewers, sources, fab files, open-science scorecard 091a7c9 12d ago
/**
 * 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>
      }
    />
  )
}