"""Minimal Gerber (RS-274X, KiCad flavour) parser + matplotlib renderer.""" import re, math from shapely.geometry import Polygon, LineString, Point, box from shapely.ops import unary_union import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import Polygon as MPoly from matplotlib.collections import PatchCollection def parse(path): macros={}; apdefs={}; cur_macro=None; mbody=[] ap=None; inreg=False; x=y=0; pol="D" objs=[] # (kind, geom-params, polarity, net) cur=[]; net=None for raw in open(path, errors="replace"): s=raw.strip() if not s: continue if cur_macro is not None: mbody.append(s) if s.endswith("%"): macros[cur_macro]="".join(mbody).rstrip("%"); cur_macro=None; mbody=[] continue m=re.match(r"%AM([^*]+)\*",s) if m: cur_macro=m.group(1); mbody=[s[len(m.group(0)):]] if s.endswith("%"): macros[cur_macro]="".join(mbody).rstrip("%"); cur_macro=None; mbody=[] continue if s.startswith("G04 #@! TO.N,"): net=s[len("G04 #@! TO.N,"):].rstrip("*"); continue if s.startswith("G04 #@! TD"): net=None; continue if s.startswith("G04"): continue m=re.match(r"%ADD(\d+)([^,*]+)(?:,([^*]*))?\*%",s) if m: apdefs[m.group(1)]=(m.group(2),[float(t) for t in (m.group(3) or "").split("X") if t]); continue if s=="%LPC*%": pol="C"; continue if s=="%LPD*%": pol="D"; continue if s.startswith("%"): continue if s=="G36*": inreg=True; cur=[]; continue if s=="G37*": inreg=False; objs.append(("region",cur,pol,net)); continue m=re.match(r"D(\d+)\*$",s) if m and int(m.group(1))>=10: ap=apdefs.get(m.group(1)); continue m=re.match(r"(?:X(-?\d+))?(?:Y(-?\d+))?D0([123])\*$",s) if m: nx=int(m.group(1)) if m.group(1) is not None else x; ny=int(m.group(2)) if m.group(2) is not None else y op=m.group(3) if inreg: cur.append((nx,ny)) elif op=="3": objs.append(("flash",(ap,nx,ny),pol,net)) elif op=="1": objs.append(("draw",(ap,x,y,nx,ny),pol,net)) x,y=nx,ny return objs, macros def ap_shape(ap, cx, cy, macros): """shapely geometry of an aperture flashed at (cx,cy) in mm (y up negative handled by caller).""" kind,par=ap if kind=="C": return Point(cx,cy).buffer(par[0]/2, resolution=8) if kind=="R": return box(cx-par[0]/2,cy-par[1]/2,cx+par[0]/2,cy+par[1]/2) if kind=="O": w,h=par[0],par[1] if w>=h: return LineString([(cx-(w-h)/2,cy),(cx+(w-h)/2,cy)]).buffer(h/2, resolution=8) return LineString([(cx,cy-(h-w)/2),(cx,cy+(h-w)/2)]).buffer(w/2, resolution=8) if kind=="RoundRect": r=par[0]; pts=[(cx+par[1+2*i],cy+par[2+2*i]) for i in range(4)] return Polygon(pts).buffer(r, resolution=6, join_style=1) if kind=="P": # regular polygon d,n=par[0],int(par[1]); rot=par[2] if len(par)>2 else 0 return Polygon([(cx+d/2*math.cos(math.radians(rot+360*i/n)), cy+d/2*math.sin(math.radians(rot+360*i/n))) for i in range(n)]) body=macros.get(kind,"") # KiCad custom macros: polygon primitives "4,1,n,x1,y1,...,rot" possibly several; also circles "1,1,d,x,y" geoms=[] for prim in body.split("*"): prim=prim.strip() if not prim or prim.startswith("0 "): continue toks=prim.split(",") try: if toks[0]=="4": n=int(float(toks[2])); coords=[float(t) for t in toks[3:3+2*n]] rot=float(toks[3+2*n]) if len(toks)>3+2*n else 0 pts=[(coords[2*i],coords[2*i+1]) for i in range(n)] th=math.radians(rot) pts=[(cx+px*math.cos(th)-py*math.sin(th), cy+px*math.sin(th)+py*math.cos(th)) for px,py in pts] if len(pts)>=3: geoms.append(Polygon(pts)) elif toks[0]=="1": d=float(toks[2]); px=float(toks[3]); py=float(toks[4]) geoms.append(Point(cx+px,cy+py).buffer(d/2, resolution=6)) elif toks[0] in ("20","21"): # vector line / center line: 21 = center line: exposure,w,h,cx,cy,rot if toks[0]=="21": w,h,px,py,rot=[float(t) for t in toks[2:7]] pts=[(-w/2,-h/2),(w/2,-h/2),(w/2,h/2),(-w/2,h/2)] th=math.radians(rot) pts=[(cx+px+a*math.cos(th)-b*math.sin(th), cy+py+a*math.sin(th)+b*math.cos(th)) for a,b in pts] geoms.append(Polygon(pts)) except Exception: pass if geoms: return unary_union(geoms) return Point(cx,cy).buffer(0.3) def geoms_of(objs, macros, window=None): """Return list of (shapely geom, polarity, net). y flipped to board coords (y down positive like KiCad).""" out=[] wx0,wy0,wx1,wy1 = window if window else (-1e9,-1e9,1e9,1e9) for kind,g,pol,net in objs: if kind=="region": pts=[(px/1e6,-py/1e6) for px,py in g] if len(pts)<3: continue xs=[p[0] for p in pts]; ys=[p[1] for p in pts] if max(xs)wx1 or max(ys)wy1: continue try: poly=Polygon(pts) except Exception: continue if not poly.is_valid: poly=poly.buffer(0) out.append((poly,pol,net)) elif kind=="flash": ap,px,py=g if ap is None: continue cx,cy=px/1e6,-py/1e6 if cxwx1+3 or cywy1+3: continue # macros defined with y up: flip y params -> we flip the whole shape: build in y-up space then mirror sh=ap_shape(ap,cx,-cy,macros) from shapely.affinity import scale sh=scale(sh, xfact=1, yfact=-1, origin=(0,0)) out.append((sh,pol,net)) elif kind=="draw": ap,x1,y1,x2,y2=g if ap is None: continue a=(x1/1e6,-y1/1e6); b=(x2/1e6,-y2/1e6) if max(a[0],b[0])wx1+1 or max(a[1],b[1])wy1+1: continue w=ap[1][0] if ap[0]=="C" else max(ap[1][:1] or [0.1]) if a==b: sh=Point(a).buffer(w/2, resolution=6) else: sh=LineString([a,b]).buffer(w/2, resolution=6) out.append((sh,pol,net)) return out def draw_geoms(ax, geoms, color, alpha=1.0, bg="white", zorder=1, net_colors=None): patches=[]; colors=[] for sh,pol,net in geoms: c = color if net_colors and net in net_colors: c=net_colors[net] if pol=="C": c=bg polys=[sh] if sh.geom_type=="Polygon" else list(getattr(sh,"geoms",[])) for p in polys: if p.is_empty or p.geom_type!="Polygon": continue patches.append(MPoly(list(p.exterior.coords), closed=True)); colors.append(c) for ring in p.interiors: patches.append(MPoly(list(ring.coords), closed=True)); colors.append(bg) pc=PatchCollection(patches, facecolors=colors, edgecolors="none", alpha=alpha, zorder=zorder) ax.add_collection(pc)