pixalynx/edge-connector

Configures USB-A, Micro-USB, and USB-C PCB-edge connectors with exposed copper contacts and automatically shaped, positioned board tongues.

Version
1.0.0
License
GPL-3.0-only
Stars
0

EdgeConnectors.test.mjs

import { expect, test } from "bun:test"
import { readFileSync } from "node:fs"
import { jsx } from "react/jsx-runtime"
import { Circuit } from "tscircuit"
import { UsbCEdgeConnector, MicroUsbEdgeConnector } from "./index.tsx"
import UsbCExample from "./examples/usb-c.circuit.tsx"
import MicroExample from "./examples/micro-usb.circuit.tsx"

async function render(element) {
  const circuit = new Circuit()
  circuit.add(element)
  await circuit.renderUntilSettled()
  return circuit.getCircuitJson()
}

function sourcePads(filename) {
  const source = readFileSync(new URL(`./vendor/pcb-edge-usb/${filename}`, import.meta.url), "utf8")
  const regex = /\(pad "([^"]+)" smd rect\s+\(at ([-\d.]+) ([-\d.]+) ([-\d.]+)\)\s+\(size ([-\d.]+) ([-\d.]+)\)\s+\(layers "([FB]).Cu"/g
  return [...source.matchAll(regex)].map(([, pin, x, y, rotation, width, height, side]) => ({
    pin, x: Number(x), y: Number(y), rotation: Number(rotation),
    width: Number(width), height: Number(height), layer: side === "F" ? "top" : "bottom",
  }))
}

const variants = [
  { name: "USB-C", component: UsbCEdgeConnector, example: UsbCExample, count: 24, shift: 2.4,
    file: "USB_C_PCB_receptacle_24pin_1mm-cutout.kicad_mod" },
  { name: "Micro-B", component: MicroUsbEdgeConnector, example: MicroExample, count: 5, shift: 0,
    file: "USB_Micro-B_PCB_receptacle_5pin_2mm_cutout.kicad_mod" },
]

for (const variant of variants) {
  for (const rotation of [0, 90]) {
    test(`${variant.name}: every pad matches upstream geometry and face at ${rotation} degrees`, async () => {
      const json = await render(jsx("board", { width: 30, height: 30,
        children: jsx(variant.component, { name: "J1", pcbX: 3, pcbY: 2, pcbRotation: rotation }),
      }))
      const pads = json.filter(e => e.type === "pcb_smtpad")
      const expected = sourcePads(variant.file)
      expect(expected).toHaveLength(variant.count)
      expect(pads).toHaveLength(variant.count)
      expect(json.filter(e => e.type === "pcb_solder_paste")).toHaveLength(0)
      const sourceIds = new Set()
      for (const source of expected) {
        const pad = pads.find(p => p.port_hints.includes(source.pin))
        const x = -source.y
        const y = -source.x - variant.shift
        expect(pad.x).toBeCloseTo(3 + (rotation === 0 ? x : -y), 5)
        expect(pad.y).toBeCloseTo(2 + (rotation === 0 ? y : x), 5)
        expect(pad.width).toBeCloseTo(rotation === 0 ? source.width : source.height)
        expect(pad.height).toBeCloseTo(rotation === 0 ? source.height : source.width)
        expect(pad.layer).toBe(source.layer)
        expect(pad.is_covered_with_solder_mask).toBe(false)
        const port = json.find(e => e.type === "pcb_port" && e.pcb_port_id === pad.pcb_port_id)
        expect(port.layers).toEqual([source.layer])
        const sourcePort = json.find(e => e.type === "source_port" && e.source_port_id === port.source_port_id)
        expect(sourcePort.port_hints).toContain(source.pin)
        sourceIds.add(sourcePort.source_port_id)
      }
      expect(sourceIds.size).toBe(variant.count)
      expect(json.filter(e => /error$/.test(e.type))).toEqual([])
    })
  }

  test(`${variant.name}: example retains cutouts and keeps contact corners inside the PCB`, async () => {
    const json = await render(jsx(variant.example, {}))
    const board = json.find(e => e.type === "pcb_board")
    expect(board.thickness).toBe(0.6)
    expect(board.outline.length).toBeGreaterThan(80)
    const inside = (x, y) => {
      let result = false
      for (let i = 0, j = board.outline.length - 1; i < board.outline.length; j = i++) {
        const a = board.outline[i], b = board.outline[j]
        if ((a.y > y) !== (b.y > y) && x < (b.x-a.x)*(y-a.y)/(b.y-a.y)+a.x) result = !result
      }
      return result
    }
    for (const p of json.filter(e => e.type === "pcb_smtpad")) {
      for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
        expect(inside(p.x + sx*p.width/2, p.y + sy*p.height/2)).toBe(true)
      }
    }
    expect(json.filter(e => /error$/.test(e.type))).toEqual([])
  })
}

test("USB-C connections keep CC1, CC2 and the two USB2 data contacts distinct", async () => {
  const json = await render(jsx("board", { width: 30, height: 30,
    children: jsx(UsbCEdgeConnector, { name: "J1", connections: {
      CC1: "net.CC_ONE", CC2: "net.CC_TWO", A6: "net.DP_TOP", B6: "net.DP_BOTTOM",
    } }),
  }))
  for (const [pin, net] of [["A5", "CC_ONE"], ["B5", "CC_TWO"], ["A6", "DP_TOP"], ["B6", "DP_BOTTOM"]]) {
    const port = json.find(e => e.type === "source_port" && e.name === pin)
    const target = json.find(e => e.type === "source_net" && e.name === net)
    const trace = json.find(e => e.type === "source_trace" && e.connected_source_port_ids.includes(port.source_port_id))
    expect(trace.connected_source_net_ids).toEqual([target.source_net_id])
  }
  expect(json.filter(e => e.type === "source_trace")).toHaveLength(4)
})