abse/gameboy-advance

A tscircuit package created by abse.

Version
0.0.45
License
unset
Stars
0

checks/fabrication-orientation.ts

PCBPCB preview for checks/fabrication-orientation.ts
SchematicSchematic preview for checks/fabrication-orientation.ts
import {readFileSync,writeFileSync} from 'node:fs'
import {createHash} from 'node:crypto'
import {join} from 'node:path'
import {analyzePcbPin1Location} from '@tscircuit/circuit-json-util'
import {getRotationBetweenPcbPin1Locations} from 'circuit-json'
import {populatePartOrientationMetadata,convertCircuitJsonToPickAndPlaceCsv,convertCircuitJsonToPickAndPlaceRows} from 'circuit-json-to-pnp-csv'

// Actual JLCPCB/EasyEDA reference footprints, fetched without datasheet enrichment.
// These snapshots keep release builds reproducible when the supplier API is unavailable.
const cacheDir=join(import.meta.dir,'supplier-orientation')
const number=(p:any)=>Number(p.port_hints?.map(String).find((s:string)=>/^pin\d+$/.test(s))?.slice(3))
const center=(p:any)=>p.points?{x:(Math.min(...p.points.map((v:any)=>v.x))+Math.max(...p.points.map((v:any)=>v.x)))/2,y:(Math.min(...p.points.map((v:any)=>v.y))+Math.max(...p.points.map((v:any)=>v.y)))/2}:{x:p.x,y:p.y}
const pads=(c:any[])=>c.filter(p=>p.type==='pcb_smtpad'||p.type==='pcb_plated_hole')
const sha=(s:string)=>createHash('sha256').update(s).digest('hex')
const normalize=(r:number)=>(r%360+360)%360
const frames=['leftside_top','bottomside_left','rightside_bottom','topside_right','leftside_bottom','bottomside_right','rightside_top','topside_left']
// Pin-1 can be inside the mounting-tab envelope, which the generic extrema-based
// analyzer cannot classify. Verify these explicit reference frames against ALL
// numbered pads, not merely an unchecked rotation override.
const reviews:any={
 C165948:{referenceFrame:'rightside_top',reason:'Project preserves legacy USB pin IDs; imported supplier pin 1 is project shell pad 9.',map:{1:9,2:11,3:10,4:8,5:19,6:18,7:17,8:16,9:12,10:13,11:14,12:15,13:23,14:22,15:20,16:21}},
 C11098:{referenceFrame:'bottomside_right',reason:'Pin 1 is rightmost in the bottom 50-contact row; pads 51/52 are mounting tabs outside that row.'},
 C136720:{referenceFrame:'rightside_top',reason:'Electrical pins 1,2,3 form a downward vertical row; pins 4/5 are mechanical case stakes.'}
}
export async function prepareFabricationOrientation(circuit:any[],out:string){
 const sources=new Map(circuit.filter(x=>x.type==='source_component').map(x=>[x.source_component_id,x]))
 const evidence:any[]=[]
 const snapshots=new Map<string,any[]>()
 for(const component of circuit.filter(x=>x.type==='pcb_component'&&!x.do_not_place)){
  const source=sources.get(component.source_component_id);const id=source?.supplier_part_numbers?.jlcpcb?.[0];if(!id)continue
  const raw=readFileSync(join(cacheDir,id+'.json'),'utf8');const supplier=JSON.parse(raw);snapshots.set(id,supplier)
  const review=reviews[id]
  if(!review){
   const radians=-component.rotation*Math.PI/180
   const local=pads(circuit).filter(p=>p.pcb_component_id===component.pcb_component_id).map(p=>{
    const q=center(p),x=q.x-component.center.x,y=(component.layer==='bottom'?-1:1)*(q.y-component.center.y)
    return {...p,points:undefined,x:x*Math.cos(radians)-y*Math.sin(radians),y:x*Math.sin(radians)+y*Math.cos(radians)}
   })
   const geometric=analyzePcbPin1Location(local)
   if(!geometric)throw Error(source.name+': authored pin-1 orientation cannot be resolved from actual pads')
   if(component.pin1_location&&component.pin1_location!==geometric)evidence.push({designator:source.name,part:id,declaredFrame:component.pin1_location,geometricFrame:geometric,reason:'Authored frame rechecked against unrotated numbered copper pads.'})
   component.pin1_location=geometric
   continue
  }
  const referencePads=pads(supplier);const localPads=pads(circuit).filter(p=>p.pcb_component_id===component.pcb_component_id)
  const pairs=referencePads.map(p=>{const n=number(p),mapped=review.map?.[n]??n;const local=localPads.find(q=>number(q)===mapped);if(!local)throw Error(`${source.name}: missing mapped supplier pad ${n}`);return {n,reference:center(p),local:center(local)}})
  if(pairs.length!==localPads.length)throw Error(source.name+': supplier pad count mismatch')
  const radians=-component.rotation*Math.PI/180
  for(const p of pairs){const x=p.local.x-component.center.x,y=p.local.y-component.center.y;p.local={x:x*Math.cos(radians)-y*Math.sin(radians),y:x*Math.sin(radians)+y*Math.cos(radians)}}
  const fits=[0,90,180,270].map(angle=>{const r=angle*Math.PI/180;const rotated=pairs.map(p=>({x:p.reference.x*Math.cos(r)-p.reference.y*Math.sin(r),y:p.reference.x*Math.sin(r)+p.reference.y*Math.cos(r)}));const dx=pairs.reduce((s,p,i)=>s+p.local.x-rotated[i].x,0)/pairs.length,dy=pairs.reduce((s,p,i)=>s+p.local.y-rotated[i].y,0)/pairs.length;return {angle,maxError:Math.max(...pairs.map((p,i)=>Math.hypot(p.local.x-rotated[i].x-dx,p.local.y-rotated[i].y-dy)))}}).sort((a,b)=>a.maxError-b.maxError)
  if(fits[0].maxError>.02||fits[1].maxError<.02)throw Error(`${source.name}: supplier pad geometry does not establish a unique orientation: ${JSON.stringify(fits)}`)
  const referenceFrame=review.referenceFrame
  const auto=analyzePcbPin1Location(supplier)
  if(auto&&auto!==referenceFrame)throw Error(source.name+': reviewed reference contradicts supplier analyzer')
  const localFrame=frames.find(frame=>normalize(getRotationBetweenPcbPin1Locations(referenceFrame,frame)??NaN)===normalize(fits[0].angle))
  if(!localFrame)throw Error(source.name+': cannot represent verified reference rotation')
  component.pin1_location=localFrame;component.supplier_pin1_location_map={...component.supplier_pin1_location_map,jlcpcb:referenceFrame}
  evidence.push({designator:source.name,part:id,referenceFrame,localFrame,matchedPads:pairs.length,maxPadCenterErrorMm:fits[0].maxError,referenceSha256:sha(raw),reason:review.reason})
 }
 const prepared=await populatePartOrientationMetadata(circuit,{supplier:'jlcpcb',partsEngine:{fetchPartCircuitJson:({supplierPartNumber}:any)=>snapshots.get(supplierPartNumber!)}})
 const rows=convertCircuitJsonToPickAndPlaceRows(prepared,{supplier:'jlcpcb',requireSupplierRotation:true})
 writeFileSync(join(out,'jlcpcb-pick-and-place.csv'),convertCircuitJsonToPickAndPlaceCsv(prepared,{supplier:'jlcpcb',requireSupplierRotation:true}))
 writeFileSync(join(out,'fabrication-orientation.json'),JSON.stringify({pass:true,supplier:'jlcpcb',strictRotationVerification:true,placedComponents:rows.length,supplierReferenceParts:snapshots.size,reviewedConnectorFrames:evidence,rotations:rows},null,2))
 return prepared
}