Drogheda/03-Architecture-Hardware/Auditoria-Final-V56-V57/scripts/gerb.py

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)