main
John Lauer README.html with embedded 3D walkthrough viewer, full BOM and schematic, source bundle, build outputs 23c1d5e 2mo ago
import fs from "fs"
const b = fs.readFileSync("dist/lib/index/3d.glb")
const g = JSON.parse(b.slice(20, 20 + b.readUInt32LE(12)).toString("utf8"))
const mat = n => { if (n.matrix) return n.matrix.slice()
  const t=n.translation||[0,0,0], r=n.rotation||[0,0,0,1], s=n.scale||[1,1,1], [x,y,z,w]=r
  return [(1-2*(y*y+z*z))*s[0],(2*(x*y+z*w))*s[0],(2*(x*z-y*w))*s[0],0,
          (2*(x*y-z*w))*s[1],(1-2*(x*x+z*z))*s[1],(2*(y*z+x*w))*s[1],0,
          (2*(x*z+y*w))*s[2],(2*(y*z-x*w))*s[2],(1-2*(x*x+y*y))*s[2],0,t[0],t[1],t[2],1] }
const mul=(a,b)=>{const o=new Array(16);for(let c=0;c<4;c++)for(let r=0;r<4;r++){let v=0;for(let k=0;k<4;k++)v+=a[k*4+r]*b[c*4+k];o[c*4+r]=v}return o}
const xf=(m,p)=>[m[0]*p[0]+m[4]*p[1]+m[8]*p[2]+m[12],m[1]*p[0]+m[5]*p[1]+m[9]*p[2]+m[13],m[2]*p[0]+m[6]*p[1]+m[10]*p[2]+m[14]]
const I=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
function bbox(idx,parent,acc){const n=g.nodes[idx],m=mul(parent,mat(n))
  if(n.mesh!=null)for(const p of g.meshes[n.mesh].primitives){const a=g.accessors[p.attributes.POSITION];if(!a.min)continue
    for(let i=0;i<8;i++){const c=[(i&1)?a.max[0]:a.min[0],(i&2)?a.max[1]:a.min[1],(i&4)?a.max[2]:a.min[2]],w=xf(m,c)
      for(let k=0;k<3;k++){acc.min[k]=Math.min(acc.min[k],w[k]);acc.max[k]=Math.max(acc.max[k],w[k])}}}
  for(const c of n.children||[])bbox(c,m,acc)}
const bb = name => { const i=g.nodes.findIndex(n=>n.name===name); if(i<0) return null
  const acc={min:[1e9,1e9,1e9],max:[-1e9,-1e9,-1e9]}; bbox(i,I,acc); return acc }
const j1=bb("J1"), bd=bb("Box0")
// GLB X = -pcbX (verified against MP1..MP4 and D0)
const j1PcbMin=-j1.max[0], j1PcbMax=-j1.min[0]
const edge=bd.max[0]
const cj=JSON.parse(fs.readFileSync("dist/lib/index/circuit.json","utf8"))
const sc=cj.find(x=>x.type==="source_component"&&x.name==="J1")
const pc=cj.find(x=>x.type==="pcb_component"&&x.source_component_id===sc.source_component_id)
const pads=cj.filter(x=>x.type==="pcb_smtpad"&&x.pcb_component_id===pc.pcb_component_id)
const padRow=pads.reduce((s,p)=>s+p.x,0)/pads.length
console.log(`board east edge   = ${edge.toFixed(3)}`)
console.log(`pad row x         = ${padRow.toFixed(3)}`)
console.log(`pcb_component ctr = ${pc.center.x.toFixed(3)}`)
console.log(`shell pcb x range = ${j1PcbMin.toFixed(3)} .. ${j1PcbMax.toFixed(3)}  (depth ${(j1PcbMax-j1PcbMin).toFixed(3)})`)
console.log(`  back vs pad row = ${(j1PcbMin-padRow).toFixed(3)} mm  (target 0)`)
console.log(`  mouth vs edge   = ${(j1PcbMax-edge).toFixed(3)} mm  (target +0.4 proud)`)

const backErr = Math.abs(j1PcbMin - padRow)
const proud = j1PcbMax - edge
let bad = false
if (backErr > 0.3) { console.error(`FAIL: shell back is ${backErr.toFixed(2)} mm off the pad row`); bad = true }
if (proud < 0.1 || proud > 1.0) { console.error(`FAIL: mouth is ${proud.toFixed(2)} mm past the edge; want 0.1..1.0`); bad = true }
const holes = cj.filter(x => (x.type === "pcb_plated_hole" || x.type === "pcb_hole") && x.pcb_component_id === pc.pcb_component_id)
const maxCopper = Math.max(
  ...pads.map(p => p.x + (p.width || 0) / 2),
  ...holes.map(h => h.x + (h.outer_width ?? h.hole_diameter ?? 0) / 2),
)
if (maxCopper > edge) { console.error(`FAIL: copper reaches ${maxCopper.toFixed(2)} > board edge ${edge}`); bad = true }
console.log(`easternmost copper = ${maxCopper.toFixed(3)} (edge ${edge}, clearance ${(edge - maxCopper).toFixed(3)})`)
if (bad) process.exit(1)
console.log("OK: USB-C placement verified against the rendered GLB")