abse/gameboy-advance
A tscircuit package created by abse.
- Version
- 0.0.45
- License
- unset
- Stars
- 0
checks/fabrication-orientation.ts
PCB
Schematic
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
}