pixalynx/usbc-led-switch

A single-sided USB-C–powered LED light board with a latching pushbutton, 100 Ω current-limiting resistor, white 5730 SMD LED, and dual 5.1 kΩ USB-C CC pull-down resistors.

Version
0.0.1
License
unset
Stars
0

scripts/check-3d.ts

// Offline 3D placement check (adapted from usb3-hub-10g): runs tscircuit's own circuit-json-to-gltf scene conversion on
// (model files served from the project by a throwaway local server) and compares every OBJ body with its pads:
//   centre offset (body XY-bounds centre vs pad-bounds centre), axis swap (body long side vs pad long side),
//   and the body's lowest point vs the board's top surface.
// Usage: bun scripts/check-3d.ts [--dump REF]
import { convertCircuitJsonTo3D } from "circuit-json-to-gltf"

const root = new URL("..", import.meta.url).pathname
const server = Bun.serve({ port: 0, fetch: (req) => new Response(Bun.file(root + decodeURIComponent(new URL(req.url).pathname).replace(/^\/+/, ""))) })
const cj: any[] = await Bun.file(root + "dist/index/circuit.json").json()
const scene = await convertCircuitJsonTo3D(cj as any, { projectBaseUrl: `http://localhost:${server.port}/`, renderBoardTextures: false } as any)
server.stop()

const src = new Map(cj.filter((e) => e.type === "source_component").map((e) => [e.source_component_id, e]))
const pcbc = new Map(cj.filter((e) => e.type === "pcb_component").map((e) => [e.pcb_component_id, e]))
const board = cj.find((e) => e.type === "pcb_board")
const top = (board.thickness ?? 1.4) / 2
const padsOf = new Map<string, number[][]>()
for (const e of cj) {
  if (e.type !== "pcb_smtpad" && e.type !== "pcb_plated_hole") continue
  let x = e.x, y = e.y, w = e.width ?? e.outer_width ?? e.outer_diameter ?? 2 * (e.radius ?? 0), h = e.height ?? e.outer_height ?? e.outer_diameter ?? 2 * (e.radius ?? 0)
  if (e.shape === "polygon") { const xs = e.points.map((p: any) => p.x), ys = e.points.map((p: any) => p.y); x = (Math.max(...xs) + Math.min(...xs)) / 2; y = (Math.max(...ys) + Math.min(...ys)) / 2; w = Math.max(...xs) - Math.min(...xs); h = Math.max(...ys) - Math.min(...ys) }
  const r = ((e.ccw_rotation ?? 0) % 180 + 180) % 180
  if (Math.abs(r - 90) < 1) [w, h] = [h, w]
  const list = padsOf.get(e.pcb_component_id) ?? []; list.push([x - w / 2, y - h / 2, x + w / 2, y + h / 2]); padsOf.set(e.pcb_component_id, list)
}
const refOf = new Map<string, string>()
for (const c of cj.filter((e) => e.type === "cad_component" && e.model_obj_url)) refOf.set((src.get(c.source_component_id) as any).name, c.pcb_component_id)

const dump = process.argv.includes("--dump") ? process.argv[process.argv.indexOf("--dump") + 1] : null
const rows: string[] = []
let worst = 0
for (const b of scene.boxes as any[]) {
  if (!b.label || !refOf.has(b.label) || !b.mesh) continue
  // world-space vertices: mesh vertices rotated (XYZ euler, radians) then translated by the box centre
  const [rx, ry, rz] = [b.rotation?.x ?? 0, b.rotation?.y ?? 0, b.rotation?.z ?? 0]
  const rot = (p: number[]) => {
    let [x, y, z] = p
    ;[y, z] = [y * Math.cos(rx) - z * Math.sin(rx), y * Math.sin(rx) + z * Math.cos(rx)]
    ;[x, z] = [x * Math.cos(ry) + z * Math.sin(ry), -x * Math.sin(ry) + z * Math.cos(ry)]
    ;[x, y] = [x * Math.cos(rz) - y * Math.sin(rz), x * Math.sin(rz) + y * Math.cos(rz)]
    return [x + b.center.x, y + b.center.y, z + b.center.z]
  }
  const vs: number[][] = []
  for (const t of b.mesh.triangles) for (const v of t.vertices) vs.push(rot([v.x, v.y, v.z]))
  // scene frame -> board frame: x = x, board y = scene z, height = scene y (circuit Z-up -> scene Y-up)
  const bx = vs.map((v) => v[0]), by = vs.map((v) => v[2]), bh = vs.map((v) => v[1])
  const pads = padsOf.get(refOf.get(b.label)!)!
  const px0 = Math.min(...pads.map((p) => p[0])), px1 = Math.max(...pads.map((p) => p[2])), py0 = Math.min(...pads.map((p) => p[1])), py1 = Math.max(...pads.map((p) => p[3]))
  const dx = (Math.max(...bx) + Math.min(...bx)) / 2 - (px0 + px1) / 2, dy = (Math.max(...by) + Math.min(...by)) / 2 - (py0 + py1) / 2
  const bw = Math.max(...bx) - Math.min(...bx), bd = Math.max(...by) - Math.min(...by), pw = px1 - px0, pd = py1 - py0
  const swapped = Math.abs(bw - bd) > 0.3 && Math.abs(pw - pd) > 0.3 && (bw > bd) !== (pw > pd)
  const zmin = Math.min(...bh) - top, zmax = Math.max(...bh) - top
  worst = Math.max(worst, Math.hypot(dx, dy))
  rows.push(`${b.label.padEnd(6)} centre off ${dx.toFixed(2).padStart(6)} ${dy.toFixed(2).padStart(6)}  body ${bw.toFixed(2)}x${bd.toFixed(2)} pads ${pw.toFixed(2)}x${pd.toFixed(2)}${swapped ? "  AXES SWAPPED" : ""}  z ${zmin.toFixed(2)}..${zmax.toFixed(2)}`)
  if (dump === b.label) console.log(JSON.stringify({ center: b.center, rotation: b.rotation, bbox: b.mesh.boundingBox }, null, 1))
}
console.log(rows.sort().join("\n"))
console.log(`${rows.length} modelled parts; worst centre offset ${worst.toFixed(2)} mm`)