main
John Lauer Publish 0.1.26 0b71841
"""Place semantic documentation blocks; never scatter or reorder their rows.
Native measured bounds are supplied by the EDA adapter. World coordinates/mm.
"""
import numpy as np
import shapely
from shapely.geometry import box
from shapely.ops import unary_union

def place_sections(sections, labels, bounds, outline, obstacle, occupied, side):
    x0,y0,x1,y1=outline.bounds
    gx,gy=np.meshgrid(np.arange(x0,x1,.35),np.arange(y0,y1,.35))
    gx,gy=gx.ravel(),gy.ravel()
    accepted=[]; unresolved=[]; reports=[]
    for section in sections:
        chosen=None
        for font in section.get('fonts',[.6,.5,.4,.35,.3]):
            rows=section['rows']; ncols=max(map(len,rows)); colwidth=[]
            for col in range(ncols):
                colwidth.append(max([bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[2]-bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[0] for row in rows for j,i in enumerate(row) if j==col]+[0])+.8)
            raw=[]; y=0
            for row in rows:
                h=max(bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[3]-bounds(i,font*section.get('sizeFactors',{}).get(str(i),1),0)[1] for i in row)
                for col,i in enumerate(row):
                    size=font*section.get('sizeFactors',{}).get(str(i),1); b=bounds(i,size,0)
                    # Bottom is viewed from below: align right in board coordinates,
                    # which becomes a shared left edge in the readable bottom view.
                    dx=sum(colwidth[:col])-(b[2] if side=='B.SilkS' else b[0]);dy=y-b[1]
                    raw.append((i,size,dx,dy,[b[0]+dx,b[1]+dy,b[2]+dx,b[3]+dy]))
                y+=h+max(.28,font*.65)
            envelope=unary_union([box(*r[4]) for r in raw]).bounds
            valid=shapely.contains(outline,shapely.box(gx+envelope[0],gy+envelope[1],gx+envelope[2],gy+envelope[3]))
            # Reserve the whole section against other labels, but permit holes
            # between rows when the actual strokes have clearance.
            valid &= ~shapely.intersects(occupied,shapely.box(gx+envelope[0]-.15,gy+envelope[1]-.15,gx+envelope[2]+.15,gy+envelope[3]+.15))
            for _,_,_,_,b in raw:
                indices=np.flatnonzero(valid)
                if not len(indices):break
                geoms=shapely.box(gx[indices]+b[0]-.045,gy[indices]+b[1]-.045,gx[indices]+b[2]+.045,gy[indices]+b[3]+.045)
                valid[indices] &= ~shapely.intersects(obstacle,geoms)
            indices=np.flatnonzero(valid)
            if len(indices):
                ax,ay=section['anchor']; n=indices[np.argmin((gx[indices]+(envelope[0]+envelope[2])/2-ax)**2+(gy[indices]+(envelope[1]+envelope[3])/2-ay)**2)]
                chosen=[]
                for i,size,dx,dy,b in raw:
                    l=labels[i];chosen.append(dict(index=i,text=l['text'],reference=None,layer=side,role=l['role'],section=section['id'],textAlign='left',x=float(gx[n]+dx),y=float(gy[n]+dy),size=size,angle=0,box=[float(b[0]+gx[n]),float(b[1]+gy[n]),float(b[2]+gx[n]),float(b[3]+gy[n])]))
                occupied=unary_union([occupied,box(envelope[0]+gx[n]-.15,envelope[1]+gy[n]-.15,envelope[2]+gx[n]+.15,envelope[3]+gy[n]+.15)])
                reports.append(dict(id=section['id'],font=font,indices=[p['index'] for p in chosen],status='placed'));break
        if chosen:accepted+=chosen
        else:
            ids=[i for row in section['rows'] for i in row];unresolved+=ids;reports.append(dict(id=section['id'],status='unresolved',indices=ids))
    return accepted,unresolved,occupied,reports

if __name__=='__main__':
    import argparse,json
    from pathlib import Path
    from shapely.geometry import shape,GeometryCollection
    p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args()
    d=json.loads(Path(a.input).read_text());metrics=d['metrics']
    def bounds(i,size,angle):
        if angle:raise ValueError('Documentation sections require readable horizontal rows')
        return [v*size for v in metrics[str(i)]]
    out=[];missing=[];reports=[]
    for side in ['F.SilkS','B.SilkS']:
        obstacle=unary_union([shape(o['polygon']) for o in d['obstacles'] if o['layer']==side])
        occupied=unary_union([box(*p['box']).buffer(.035) for p in d.get('placements',[]) if p['layer']==side])
        ps,ms,_,rs=place_sections([s for s in d['sections'] if s['layer']==side],d['labels'],bounds,shape(d['outline']),obstacle,occupied,side)
        out+=ps;missing+=ms;reports+=rs
    Path(a.out).write_text(json.dumps(dict(placements=out,unresolved=missing,sections=reports),indent=2))
    print(('ERROR' if missing else 'OK')+f': {len(out)} documentation labels in ordered sections; {len(missing)} unresolved')
    raise SystemExit(2 if missing else 0)