hrithik18k/punck-tscircuit-recreation

A four-layer Eurorack drum/sampler controller PCB built around an STM32H743, dual QSPI flash, PCM5100 stereo DAC, TL074 analog audio circuitry, input clamps, 12V-to-3.3V power regulation, MIDI/USB/SWD connectors, controls, LEDs, and trigger/audio I/O.

Version
1.0.3
License
unset
Stars
0

scripts/generate-native.ts

import fs from 'node:fs'
import {parse,children,child,atom} from './sexpr'
import clipping from 'polygon-clipping'
fs.mkdirSync('native',{recursive:true})
const safe=(s:string)=>s.replace(/[^a-zA-Z0-9_]/g,'_')
function localize(e:any, cx:number,cy:number):any {
 const o=structuredClone(e)
 for(const k of Object.keys(o)) if(k.endsWith('_id')||k.endsWith('_ids')) delete o[k]
 if(typeof o.x==='number')o.x-=cx
 if(typeof o.y==='number')o.y-=cy
 for(const k of ['center','start','end']) if(o[k])o[k]={...o[k],x:o[k].x-cx,y:o[k].y-cy}
 for(const k of ['points','route']) if(o[k])o[k]=o[k].map((p:any)=>({...p,x:p.x-cx,y:p.y-cy}))
 return o
}
for(const name of ['components','controls']) {
 const title=name[0].toUpperCase()+name.slice(1)
 const cj=JSON.parse(fs.readFileSync(`prepared/${name}.circuit.json`,'utf8'))
 const pcb=parse(fs.readFileSync(`source/Punck_${title}.kicad_pcb`,'utf8'))
 const fps=children(pcb,'footprint')
 const scs=cj.filter((e:any)=>e.type==='source_component')
 const sources=new Map(cj.filter((e:any)=>e.type==='source_port').map((e:any)=>[e.source_port_id,e]))
 const netOfPort=new Map<string,string>()
 const nets=cj.filter((e:any)=>e.type==='source_net')
 for(const t of cj.filter((e:any)=>e.type==='source_trace')) {
  const n=nets.find((n:any)=>t.connected_source_net_ids.includes(n.source_net_id))
  if(n) for(const p of t.connected_source_port_ids)netOfPort.set(p,n.name)
 }
 const parts=scs.map((s:any,i:number)=>{
  const pc=cj.find((e:any)=>e.type==='pcb_component'&&e.source_component_id===s.source_component_id)
  const f=fps[i]
  const rawPads=children(f,'pad')
  const ports=cj.filter((e:any)=>e.type==='source_port'&&e.source_component_id===s.source_component_id)
  const pins=ports.map((p:any,pi:number)=>{
   const id=p.name.replace(/^pin(?=\d)/,'')
   const raw=rawPads.find(pad=>atom(pad[1])===id)
   return {id, selector:`pin${/^\d+$/.test(id)?id:pi+1}`, label:atom(raw&&child(raw,'pinfunction')?.[1])||id, net:netOfPort.get(p.source_port_id), nc:!!p.do_not_connect||atom(raw&&child(raw,'pintype')?.[1]).includes('no_connect')}
  })
  const allowed=['pcb_smtpad','pcb_plated_hole','pcb_hole','pcb_silkscreen_path','pcb_silkscreen_circle']
  const footprint=cj.filter((e:any)=>e.pcb_component_id===pc.pcb_component_id&&allowed.includes(e.type)).map((e:any)=>{
   const o=localize(e,pc.center.x,pc.center.y)
   if(o.port_hints)o.port_hints=o.port_hints.map((h:string)=>pins.find((p:any)=>p.id===h.replace(/^pin(?=\d)/,''))?.selector??h)
   return o
  })
  // Merge the two compound JP1 pad pieces per pin into one native polygon.
  // Circles use 128 points (maximum radial approximation error <0.0002mm).
  for(const pin of pins){
   const pieces=footprint.filter((e:any)=>e.type==='pcb_smtpad'&&e.port_hints?.includes(pin.selector))
   if(pieces.length<=1)continue
   const rings=pieces.map((e:any)=>[e.shape==='circle'?Array.from({length:129},(_,i)=>[e.x+e.radius*Math.cos(i*Math.PI/64),e.y+e.radius*Math.sin(i*Math.PI/64)]):e.points?.map((p:any)=>[p.x,p.y])])
   if(rings.some((r:any)=>!r[0]))continue
   const polys=clipping.union(...rings as any)
   if(polys.length!==1||polys[0].length!==1)continue
   for(const e of pieces)footprint.splice(footprint.indexOf(e),1)
   footprint.push({type:'pcb_smtpad',shape:'polygon',layer:pc.layer,port_hints:[pin.selector],points:polys[0][0].map(([x,y])=>({x,y}))})
  }
  let section='Passives'
  if(name==='controls') section=s.name.includes('Pot')?'Potentiometers':s.name.startsWith('J')?'Connectors':'Buttons_and_LEDs'
  else if(s.name==='U2')section='Microcontroller'
  else if(['U3','U5','JP1'].includes(s.name))section='QSPI_Flash'
  else if(['U1','U4'].includes(s.name))section='Audio'
  else if(['U6','U7','J7','D1','D2','L1','L2','C1','C2','C3','C4','C37','C38','C39','C40','C41','C42','C43'].includes(s.name))section='Power'
  else if(s.name.startsWith('J'))section='Connectors'
  else if(s.name.startsWith('TP'))section='Test_Points'
  else if(s.name.startsWith('Q'))section='Input_Clamps'
  else if(s.name==='Y1')section='Clock'
  return {name:s.name,value:s.display_value??s.name,ftype:s.ftype,x:pc.center.x,y:pc.center.y,layer:pc.layer,pins,footprint,section}
 })
 const sections=[...new Set(parts.filter((p:any)=>p.pins.length).map((p:any)=>p.section))]
 for(const [si,section] of sections.entries()){
  const members=parts.filter((p:any)=>p.section===section&&p.pins.length)
  const maxPins=Math.max(...members.map((p:any)=>p.pins.length))
  const pitchY=Math.max(6,Math.ceil(maxPins/2)*0.5+4)
  members.forEach((p:any,i:number)=>{p.schX=(si%3)*65+(i%4)*15;p.schY=-Math.floor(si/3)*130-Math.floor(i/4)*pitchY;p.schRotation=0})
 }
 const checkPath='evidence/native-sch-placement-components.log'
 if(name==='components'&&fs.existsSync(checkPath)){
  const log=fs.readFileSync(checkPath,'utf8')
  for(const m of log.matchAll(/<TwoPinComponentShouldBeVertical componentName="([^"]+)"[^>]*deltaSchRotation="([^"]+)"/g)){
   const p=parts.find((p:any)=>p.name===m[1]);if(p)(p as any).schRotation=Number(m[2])
  }
 }
 const board=cj.find((e:any)=>e.type==='pcb_board')
 const overrides=JSON.parse(fs.readFileSync('native/layout-overrides.json','utf8'))[name]
 for(const p of parts){
  Object.assign(p,overrides[p.name]??{})
  const seen=new Set<string>()
  for(const e of p.footprint.filter((e:any)=>e.port_hints?.length)){
   const hint=e.port_hints[0]
   if(seen.has(hint)){
    const pin=p.pins.find((q:any)=>q.selector===hint)!
    const selector=`pin${Math.max(...p.pins.map((q:any)=>Number(q.selector.slice(3))))+1}`
    p.pins.push({...pin,selector,label:`${pin.label}_DUP`} as any)
    e.port_hints=[selector]
   }else seen.add(hint)
  }
 }
 fs.writeFileSync(`native/${name}.json`,JSON.stringify({parts,sections,width:board.width,height:board.height,layers:board.num_layers,thickness:board.thickness},null,2))
 fs.writeFileSync(`native/${name}.circuit.tsx`,`import data from './${name}.json'\nimport { NativeBoard } from './NativeBoard'\nexport default () => <NativeBoard data={data} />\n`)
 console.log(name,parts.length,'parts',parts.reduce((n:any,p:any)=>n+p.pins.length,0),'pins')
}