#!/usr/bin/env python3
# panel_stamp.py — stamp N copies of a CLEAN board into one panel .kicad_pcb.
#
# This is the step the manifest-only tiler deferred: it produces a REAL panel
# board by copying every item (footprints, tracks, vias, zones, drawings) of the
# proven-clean unit board to each board-slot, translated (and optionally rotated)
# into position. Board-slots follow the same Vogel spiral used at every level
# (boards-in-panel = the recursion one level up), OR a dense grid when the goal
# is max boards per fab panel.
#
# Because each stamped board is a byte-identical copy of a board already verified
# CLEAN, the panel inherits cleanliness by construction — no re-routing, no
# re-verification of intra-board copper needed. Only inter-board spacing (gutter)
# must clear, which the placement guarantees.
import pcbnew, math, argparse, sys
MM=pcbnew.FromMM; ToMM=pcbnew.ToMM
PHI=(1+5**0.5)/2; GA=2*math.pi*(1-1/PHI/PHI)

def board_bbox_mm(b):
    bb=b.GetBoardEdgesBoundingBox()
    return ToMM(bb.GetWidth()), ToMM(bb.GetHeight()), \
           ToMM(bb.GetCenter().x), ToMM(bb.GetCenter().y)

def stamp(src_path, out_path, nboards, layout='grid', gutter=15.0, rot_each=False):
    src=pcbnew.LoadBoard(src_path)
    W,H,cxs,cys=board_bbox_mm(src)
    dia=max(W,H)
    pitch=dia+gutter
    # positions
    pos=[]
    if layout=='vogel':
        c=pitch
        for k in range(nboards):
            th=k*GA; r=c*math.sqrt(k)
            pos.append((r*math.cos(th), r*math.sin(th), math.degrees(th) if rot_each else 0.0))
    else:  # grid (densest for a rectangular fab panel)
        cols=int(math.ceil(math.sqrt(nboards)))
        for k in range(nboards):
            gx=(k%cols); gy=(k//cols)
            pos.append((gx*pitch, gy*pitch, 0.0))
    panel=pcbnew.BOARD()
    panel.SetCopperLayerCount(src.GetCopperLayerCount())
    # copy design settings (clearance etc.)
    try: panel.GetDesignSettings().m_MinClearance=src.GetDesignSettings().m_MinClearance
    except Exception: pass
    ref_tag=0
    for (dx,dy,rot) in pos:
        ref_tag+=1
        offv=pcbnew.VECTOR2I(MM(dx-cxs+ (dia/2)), MM(dy-cys+ (dia/2)))
        # Per-board net remap: nets are board-local. Register a fresh
        # NETINFO_ITEM per source net into the panel and map old code -> new,
        # so every copied item points at a net that exists in the panel.
        netmap={}
        for i in range(src.GetNetCount()):
            ni=src.FindNet(i)
            if ni is None: continue
            nm=ni.GetNetname()
            newname = nm if nm in ('GND','+12V') else "B%d_%s"%(ref_tag,nm)
            existing=panel.FindNet(newname)
            if existing is None:
                nn=pcbnew.NETINFO_ITEM(panel, newname)
                panel.Add(nn); netmap[ni.GetNetCode()]=nn
            else:
                netmap[ni.GetNetCode()]=existing
        def _remap(it):
            try:
                oc=it.GetNetCode()
                if oc in netmap: it.SetNet(netmap[oc])
            except Exception: pass
        for f in src.GetFootprints():
            nf=f.Duplicate()
            if rot: nf.Rotate(f.GetPosition(), pcbnew.EDA_ANGLE(rot, pcbnew.DEGREES_T))
            nf.Move(offv)
            nf.SetReference("B%d_%s"%(ref_tag, f.GetReference()))
            for p in nf.Pads(): _remap(p)
            panel.Add(nf)
        for t in src.GetTracks():
            nt=t.Duplicate()
            if rot: nt.Rotate(pcbnew.VECTOR2I(0,0), pcbnew.EDA_ANGLE(rot, pcbnew.DEGREES_T))
            nt.Move(offv); _remap(nt); panel.Add(nt)
        for z in src.Zones():
            nz=z.Duplicate()
            if rot: nz.Rotate(pcbnew.VECTOR2I(0,0), pcbnew.EDA_ANGLE(rot, pcbnew.DEGREES_T))
            nz.Move(offv); _remap(nz); panel.Add(nz)
        for d in src.GetDrawings():
            nd=d.Duplicate()
            if rot: nd.Rotate(pcbnew.VECTOR2I(0,0), pcbnew.EDA_ANGLE(rot, pcbnew.DEGREES_T))
            nd.Move(offv); panel.Add(nd)
    panel.BuildConnectivity()
    pcbnew.SaveBoard(out_path, panel)
    pw,ph,_,_=board_bbox_mm(panel)
    print("panel: %d boards (%s), each %.0fmm, panel %.0f x %.0f mm, %d footprints"%(
        nboards, layout, dia, pw, ph, len(list(panel.GetFootprints()))))
    return panel

if __name__=='__main__':
    ap=argparse.ArgumentParser()
    ap.add_argument('--src', default='/tmp/phyllo_n8.kicad_pcb')
    ap.add_argument('--out', default='/tmp/panel.kicad_pcb')
    ap.add_argument('--boards', type=int, default=4)
    ap.add_argument('--layout', choices=['grid','vogel'], default='grid')
    ap.add_argument('--gutter', type=float, default=15.0)
    ap.add_argument('--rotate', action='store_true')
    a=ap.parse_args()
    stamp(a.src, a.out, a.boards, a.layout, a.gutter, a.rotate)
