145 lines
7.0 KiB
Python
145 lines
7.0 KiB
Python
"""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)<wx0 or min(xs)>wx1 or max(ys)<wy0 or min(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 cx<wx0-3 or cx>wx1+3 or cy<wy0-3 or cy>wy1+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])<wx0-1 or min(a[0],b[0])>wx1+1 or max(a[1],b[1])<wy0-1 or min(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)
|