Circuit board 2

another go at circuit boards

Log in to post a comment.

// Generative PCB Artwork — rev‑3 (27 Apr 2025)
// Fixes:
//   • Components & pads now ALWAYS stay inside the board outline (margin).
//   • Pads remember their parent component, so we can exit traces cleanly.
//   • Traces leave each pad at least one GRID cell before routing, so no
//     lines cross component bodies.
//   • BFS router uses those exit points and respects component clearance.
//   • Added pad‑to‑edge clearance to placement, so nothing pokes outside.
// -----------------------------------------------------------
const BOARD_W  = 180;
const BOARD_H  = 120;
const GRID     = 4;
const MARGIN   = GRID * 3;          // board edge clearance for bodies & pads

// component counts
const IC_COUNT = 4;
const R_COUNT  = 6;
const C_COUNT  = 4;

// net params
const NETS_PER_COMP = 2;

// pad size
const PAD_R_MIN = 0.8;
const PAD_R_MAX = 1.2;

// turtle helpers -------------------------------------------------------------
const turtle = new Turtle();
Turtle.prototype.jump = function(x,y){this.penup();this.goto(x,y);this.pendown();};

// board coords & grid --------------------------------------------------------
const X0 = -BOARD_W/2 + MARGIN;
const Y0 = -BOARD_H/2 + MARGIN;
const X1 =  BOARD_W/2 - MARGIN;
const Y1 =  BOARD_H/2 - MARGIN;
function snap(v){return Math.round(v/GRID)*GRID;}
function inBoard(x,y){return x>=X0&&x<=X1&&y>=Y0&&y<=Y1;}

// occupancy grid -------------------------------------------------------------
const NX = Math.floor((X1-X0)/GRID)+1;
const NY = Math.floor((Y1-Y0)/GRID)+1;
const occ = Array.from({length:NX},()=>new Uint8Array(NY));
function gi(x){return Math.round((x-X0)/GRID);}  // 0..NX‑1
function gj(y){return Math.round((y-Y0)/GRID);}  // 0..NY‑1
function mark(x,y){const i=gi(x), j=gj(y); if(i>=0&&i<NX&&j>=0&&j<NY) occ[i][j]=1; }
function isOcc(x,y){const i=gi(x), j=gj(y); return (i<0||i>=NX||j<0||j>=NY) || occ[i][j]; }

// data -----------------------------------------------------------------------
const comps=[]; // {id,type,x,y,w,h,pads:[{x,y,comp}],bbox:{}}
const nets=[];  // {a:{x,y,comp}, b:{x,y,comp}}
let compId=0;

// utilities ------------------------------------------------------------------
function drawRect(cx,cy,w,h){
    turtle.jump(cx-w/2, cy-h/2);
    turtle.goto(cx+w/2, cy-h/2);
    turtle.goto(cx+w/2, cy+h/2);
    turtle.goto(cx-w/2, cy+h/2);
    turtle.goto(cx-w/2, cy-h/2);
}
function shuffle(arr){for(let i=arr.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[arr[i],arr[j]]=[arr[j],arr[i]];}return arr;}

// component placement --------------------------------------------------------
function canPlace(gx,gy,wg,hg){
    for(let x=gx-GRID;x<gx+(wg+1)*GRID;x+=GRID){
        for(let y=gy-GRID;y<gy+(hg+1)*GRID;y+=GRID){ if(isOcc(x,y)) return false; }
    }
    return true;
}

function reserveArea(gx,gy,wg,hg){ // reserve body + 1‑cell clearance
    for(let x=gx-GRID;x<gx+(wg+1)*GRID;x+=GRID){
        for(let y=gy-GRID;y<gy+(hg+1)*GRID;y+=GRID){ mark(x,y); }
    }
}

function addPad(cx,cy,comp){
    const r = PAD_R_MIN + Math.random()*(PAD_R_MAX-PAD_R_MIN);
    turtle.jump(cx,cy-r);
    turtle.circle(r);
    mark(cx,cy);
    return {x:cx,y:cy,comp};
}

function addComponent(type,wg,hg){
    for(let t=0;t<400;t++){
        const gx = snap(X0+GRID) + Math.floor(Math.random()*(NX-wg-2))*GRID;
        const gy = snap(Y0+GRID) + Math.floor(Math.random()*(NY-hg-2))*GRID;
        if(!canPlace(gx,gy,wg,hg)) continue;
        reserveArea(gx,gy,wg,hg);
        const c = {id:compId++, type, x:gx, y:gy, w:wg*GRID, h:hg*GRID, pads:[]};
        comps.push(c);
        drawComponent(c);
        return c;
    }
    return null;
}

function drawComponent(c){
    drawRect(c.x+c.w/2, c.y+c.h/2, c.w, c.h);
    if(c.type==='IC'){
        // pads on each side every GRID cell
        const perim=[];
        for(let x=c.x;x<=c.x+c.w;x+=GRID) perim.push({x,y:c.y-GRID});          // top
        for(let y=c.y;y<=c.y+c.h;y+=GRID) perim.push({x:c.x+c.w+GRID,y});      // right
        for(let x=c.x+c.w;x>=c.x;x-=GRID) perim.push({x,y:c.y+c.h+GRID});      // bottom
        for(let y=c.y+c.h;y>=c.y;y-=GRID) perim.push({x:c.x-GRID,y});          // left
        shuffle(perim).slice(0,Math.max(4,perim.length/3)).forEach(p=>{
            if(inBoard(p.x,p.y)) c.pads.push(addPad(p.x,p.y,c));
        });
    }else{ // R/C two pads opposite
        const p1={x:c.x-GRID,y:c.y+c.h/2};
        const p2={x:c.x+c.w+GRID,y:c.y+c.h/2};
        if(inBoard(p1.x,p1.y)) c.pads.push(addPad(p1.x,p1.y,c));
        if(inBoard(p2.x,p2.y)) c.pads.push(addPad(p2.x,p2.y,c));
    }
}

// netlist --------------------------------------------------------------------
function buildNets(){
    for(const c of comps){
        for(let k=0;k<NETS_PER_COMP;k++){
            const other = comps[Math.floor(Math.random()*comps.length)];
            if(other===c) continue;
            const padA = c.pads[Math.floor(Math.random()*c.pads.length)];
            const padB = other.pads[Math.floor(Math.random()*other.pads.length)];
            nets.push({a:padA,b:padB});
        }
    }
}

// routing helpers ------------------------------------------------------------
const DIRS = [[GRID,0],[-GRID,0],[0,GRID],[0,-GRID]];
function exitPoint(pad){
    const comp = pad.comp;
    let ex=pad.x, ey=pad.y;
    if(pad.x < comp.x)              ex = pad.x - GRID;
    else if(pad.x > comp.x+comp.w)  ex = pad.x + GRID;
    else if(pad.y < comp.y)         ey = pad.y - GRID;
    else                            ey = pad.y + GRID;
    return {x:ex,y:ey};
}

function bfs(ax,ay,bx,by){
    const q=[[ax,ay]]; const seen=new Set([`${ax},${ay}`]); const prev={};
    while(q.length){
        const [x,y]=q.shift(); if(x===bx&&y===by) break;
        for(const[dx,dy] of DIRS){ const nx=x+dx, ny=y+dy; const key=`${nx},${ny}`;
            if(!inBoard(nx,ny)||isOcc(nx,ny)||seen.has(key)) continue;
            seen.add(key); prev[key]=`${x},${y}`; q.push([nx,ny]); }
    }
    const path=[]; let key=`${bx},${by}`;
    if(!prev[key]) return [];
    while(key!==`${ax},${ay}`){ const [px,py]=key.split(',').map(Number); path.unshift([px,py]); key=prev[key]; }
    return path;
}

function drawTrace(pts){
    turtle.jump(pts[0][0],pts[0][1]);
    for(let i=1;i<pts.length;i++){ turtle.goto(pts[i][0],pts[i][1]); mark(pts[i][0],pts[i][1]); }
}

function routeNets(){
    nets.forEach(net=>{
        const e1 = exitPoint(net.a); const e2 = exitPoint(net.b);
        const path = bfs(e1.x,e1.y,e2.x,e2.y);
        if(path.length){
            drawTrace([[net.a.x,net.a.y], [e1.x,e1.y], ...path, [e2.x,e2.y], [net.b.x,net.b.y]]);
        }
    });
}

// outline --------------------------------------------------------------------
function drawOutline(){
    const bx0=-BOARD_W/2, by0=-BOARD_H/2, bx1=BOARD_W/2, by1=BOARD_H/2;
    turtle.jump(bx0,by0); turtle.goto(bx1,by0); turtle.goto(bx1,by1); turtle.goto(bx0,by1); turtle.goto(bx0,by0);
}

// main -----------------------------------------------------------------------
function walk(i){
    if(i===0){
        drawOutline();
        for(let k=0;k<IC_COUNT;k++) addComponent('IC', 3+Math.floor(Math.random()*3), 3+Math.floor(Math.random()*2));
        for(let k=0;k<R_COUNT;k++)  addComponent('R',  2,1);
        for(let k=0;k<C_COUNT;k++)  addComponent('C',  1,1);
        buildNets();
        routeNets();
    }
    return false;
}