imrishabh18/pedometer
This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.
- Version
- 1.1.3
- License
- unset
- Stars
- 0
bga-autorouter.ts
import { repairViaClearances } from "./via-clearance";
import { planBgaTerminalEscapes } from "./bga-fanout";
import { enableThroughViaDrc } from "./through-via-drc";
import { AutoroutingPipelineSolver7_MultiGraph, AutoroutingPipelineSolver9_PreloadedTraceGraph, type SimpleRouteJson, type SimplifiedPcbTrace } from "@tscircuit/capacity-autorouter";
/** Detect enclosed BGA balls and QFN center pads from footprint geometry.
* Terminal-via hints are permitted only when the caller enables via-in-pad. */
export function prepareBgaRouting(input: SimpleRouteJson): SimpleRouteJson {
const srj = structuredClone(input);
const bgaEscapes = planBgaTerminalEscapes(input);
// Core represents an unbroken rectangular pour as touching horizontal
// strips. Coalesce exact rectangular tilings to avoid excessive graph
// subdivision, while preserving the obstacle's area, layer and net.
const pours = srj.obstacles.filter(o=>o.isCopperPour);
if(pours.length>1 && pours.every(o=>JSON.stringify(o.layers)===JSON.stringify(pours[0].layers) && JSON.stringify(o.connectedTo)===JSON.stringify(pours[0].connectedTo))){
const left=Math.min(...pours.map(o=>o.center.x-o.width/2)),right=Math.max(...pours.map(o=>o.center.x+o.width/2));
const bottom=Math.min(...pours.map(o=>o.center.y-o.height/2)),top=Math.max(...pours.map(o=>o.center.y+o.height/2));
const ordered=[...pours].sort((a,b)=>a.center.y-b.center.y);
const tiled=ordered.every((o,i)=>Math.abs(o.width-(right-left))<1e-6 && Math.abs(o.center.x-(left+right)/2)<1e-6 && (i===0||Math.abs(o.center.y-o.height/2-(ordered[i-1].center.y+ordered[i-1].height/2))<1e-6));
if(tiled){srj.obstacles=srj.obstacles.filter(o=>!o.isCopperPour);srj.obstacles.push({...pours[0],center:{x:(left+right)/2,y:(bottom+top)/2},width:right-left,height:top-bottom});}
}
// Route with margin above the board's 0.075 mm DRC minimum.
srj.defaultObstacleMargin=0.10;
srj.minTraceToPadEdgeClearance=0.10;
srj.minViaEdgeToPadEdgeClearance=0.10;
const groups = new Map<string, typeof srj.obstacles>();
for (const pad of srj.obstacles) {
if (!pad.componentId || !pad.circuitJsonMetadata?.pcb_smtpad_id) continue;
const pads = groups.get(pad.componentId) ?? [];
pads.push(pad); groups.set(pad.componentId, pads);
}
const qfnPeripheralPads = new Set<(typeof srj.obstacles)[number]>();
for (const pads of input.allowViaInPad === true ? groups.values() : []) {
if (pads.length < 9) continue;
for (const pad of pads) {
const {x,y} = pad.center;
// Large exposed center lands are also enclosed by peripheral QFN pads.
// A solver-generated terminal via avoids routing ground across those pins.
const exposedCenter = pad.width>=1 && pad.height>=1 &&
pads.some(p=>p.center.x+p.width/2<x-pad.width/2) &&
pads.some(p=>p.center.x-p.width/2>x+pad.width/2) &&
pads.some(p=>p.center.y+p.height/2<y-pad.height/2) &&
pads.some(p=>p.center.y-p.height/2>y+pad.height/2);
const bgaEscape=bgaEscapes.get(pad.circuitJsonMetadata!.pcb_smtpad_id!);
if (!bgaEscape && !exposedCenter) continue;
if (exposedCenter) for (const other of pads) if (other !== pad) qfnPeripheralPads.add(other);
let used = false;
for (const connection of srj.connections) for (const point of connection.pointsToConnect) {
if (Math.hypot(point.x-x,point.y-y)>0.001 || !("layer" in point) || point.layer!=="top") continue;
point.layer=bgaEscape?.layer ?? "bottom";
point.terminalVia=bgaEscape?.terminalVia ?? {toLayer:"top",viaDiameter:srj.minViaPadDiameter ?? 0.3};
used=true;
}
if (used) srj.obstacles.push({
type:"rect", obstacleId:`bga-terminal-${pad.circuitJsonMetadata?.pcb_smtpad_id}`,
center:{x,y}, width:srj.minViaPadDiameter ?? 0.3, height:srj.minViaPadDiameter ?? 0.3,
layers:["top","inner1","inner2","bottom"], connectedTo:[...pad.connectedTo],
});
}
}
// Keep routes out of the nominal 0.25 mm gaps between QFN pins.
// This is a planning envelope only; physical copper and DRC limits are unchanged.
for (const pad of qfnPeripheralPads) { pad.width += 0.10; pad.height += 0.10; }
// Reserve drill-to-copper clearance during global planning, where space
// can still be allocated. Local via movement alone cannot fix a packed
// junction. Terminal via diameters above retain their physical pad size.
const physicalPad = input.minViaPadDiameter ?? 0.3;
const drillRadius = (input.minViaHoleDiameter ?? 0.15) / 2;
srj.minViaPadDiameter = Math.max(physicalPad, 2 * (drillRadius + 0.205 - 0.10));
// The current solver prefers snake_case and some stages read the legacy
// field. Keep all three aliases consistent so this clearance is applied.
srj.min_via_pad_diameter = srj.minViaPadDiameter;
srj.minViaDiameter = srj.minViaPadDiameter;
return srj;
}
/** The solver emits a terminal via without the wire endpoint on the pad side.
* Include that endpoint so tscircuit can associate the physical pad with the
* generated through-via. This preserves the solver's copper geometry. */
export function attachPadEndpoints(traces: SimplifiedPcbTrace[], input: SimpleRouteJson) {
const points=input.connections.flatMap(c=>c.pointsToConnect);
for(const trace of traces){
const first=trace.route[0], last=trace.route.at(-1);
if(first?.route_type==="via"){
const next=trace.route[1];
const point=points.find(p=>Math.hypot(p.x-first.x,p.y-first.y)<0.001 && "layer" in p && p.layer!==(next?.route_type==="wire" ? next.layer : undefined));
if(point && "layer" in point && next?.route_type==="wire"){
first.from_layer=point.layer;first.to_layer=next.layer;
trace.route.unshift({route_type:"wire",x:point.x,y:point.y,layer:point.layer,width:input.minTraceWidth,start_pcb_port_id:point.pcb_port_id});
}
}
if(last?.route_type==="via"){
const prev=trace.route.at(-2);
const point=points.find(p=>Math.hypot(p.x-last.x,p.y-last.y)<0.001 && "layer" in p && p.layer!==(prev?.route_type==="wire" ? prev.layer : undefined));
if(point && "layer" in point && prev?.route_type==="wire"){
last.from_layer=prev.layer;last.to_layer=point.layer;
trace.route.push({route_type:"wire",x:point.x,y:point.y,layer:point.layer,width:input.minTraceWidth,end_pcb_port_id:point.pcb_port_id});
}
}
}
return traces;
}
export async function bgaAutorouter(input: SimpleRouteJson) {
const routing = prepareBgaRouting(input);
// Reserve physical inner1 for the RF ground reference. Route on a virtual
// three-layer stack (top, physical inner2, bottom), then restore layer names.
// The physical board and all generated vias remain four-layer through vias.
routing.layerCount = 3;
const mapLayer = (layer: string) => layer === "inner2" ? "inner1" : layer;
const mapLayers = (layers: string[]) => layers.filter(l=>l!=="inner1").map(mapLayer);
routing.obstacles = routing.obstacles.map(o=>{
const layers = mapLayers(o.layers);
return {...o,layers,zLayers:layers.map(l=>l==="top"?0:l==="bottom"?2:1),__zLayers:layers.map(l=>l==="top"?0:l==="bottom"?2:1)};
}).filter(o=>o.layers.length);
for (const c of routing.connections) for (const p of c.pointsToConnect) {
if ("layers" in p) p.layers = mapLayers(p.layers);
else { p.layer = mapLayer(p.layer); if(p.terminalVia)p.terminalVia.toLayer=mapLayer(p.terminalVia.toLayer); }
}
for (const trace of routing.traces ?? []) for (const p of trace.route) {
if (p.route_type === "wire") p.layer = mapLayer(p.layer);
if (p.route_type === "via") {
p.from_layer = mapLayer(p.from_layer);
p.to_layer = mapLayer(p.to_layer);
}
}
return runCapacityRouter(input,routing,true);
}
/** Route the clock nets first on top copper; the crystal allows no vias. */
export async function clockAutorouter(input: SimpleRouteJson) {
return routePlanar(input,input.minTraceWidth);
}
/** Prevent the router from narrowing the RF feed below its intended width. */
export async function rfAutorouter(input: SimpleRouteJson) {
return routePlanar(input,0.2);
}
function routePlanar(input: SimpleRouteJson, width: number) {
const planar=structuredClone(input);
planar.layerCount=1;
planar.minTraceWidth=width;
planar.nominalTraceWidth=width;
for(const c of planar.connections)c.nominalTraceWidth=width;
planar.defaultObstacleMargin=0.10;
planar.minTraceToPadEdgeClearance=0.10;
planar.obstacles=planar.obstacles.filter(o=>o.layers.includes("top")).map(o=>({...o,layers:["top"],__zLayers:[0]}));
return runCapacityRouter(input,planar);
}
async function runCapacityRouter(input: SimpleRouteJson, routingInput: SimpleRouteJson, restoreInner2=false) {
// Pipeline 7 treats previously routed clock copper as fixed obstacles.
const Solver = routingInput.layerCount === 1 ? AutoroutingPipelineSolver9_PreloadedTraceGraph : AutoroutingPipelineSolver7_MultiGraph;
const solver = new Solver(routingInput, {powerTraceExpansion:{onlyConnectionNames:[]}});
if (routingInput.layerCount > 1) enableThroughViaDrc(solver,routingInput.layerCount);
const handlers: Record<string, ((event: any)=>void)[]> = {complete:[],error:[],progress:[]};
let stopped=false;
let lastReport=Date.now();
const run=()=>{
if(stopped)return;
try {
const until=Date.now()+40;
while(!solver.solved&&!solver.failed&&Date.now()<until)solver.step();
if(solver.failed){handlers.error.forEach(h=>h({error:new Error(solver.error||"Routing failed")}));return;}
if(solver.solved){
// The solver includes preloaded copper in its output. Core retains it
// across phases already, so emit only new or explicitly replaced routes.
const preloadedIds = new Set((input.traces ?? []).map(t=>t.pcb_trace_id));
const traces = (solver.getOutputSimpleRouteJson().traces ?? []).filter(t=>
!preloadedIds.has(t.pcb_trace_id) || t.__replaces_pcb_trace_id);
if(restoreInner2)for(const trace of traces)for(const p of trace.route){
if(p.route_type==="wire"&&p.layer==="inner1")p.layer="inner2";
if(p.route_type==="via"){
if(p.from_layer==="inner1")p.from_layer="inner2";
if(p.to_layer==="inner1")p.to_layer="inner2";
}
}
// Remove the planning-only clearance envelope; all drill centers are
// still solver-generated and the physical via dimensions remain fixed.
for (const trace of traces) for (const point of trace.route) {
if (point.route_type === "via") point.via_diameter = input.minViaPadDiameter ?? 0.3;
}
const repaired = repairViaClearances(attachPadEndpoints(traces,input),input);
handlers.complete.forEach(h=>h({traces:repaired}));
return;
}
if(Date.now()-lastReport>10000){console.log("BGA autorouter phase:",solver.getCurrentPhase());lastReport=Date.now();}
setTimeout(run,0);
} catch(error){handlers.error.forEach(h=>h({error}));}
};
return {on:(event:string,handler:(event:any)=>void)=>{handlers[event].push(handler);},start:()=>setTimeout(run,0),stop:()=>{stopped=true;}};
}