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

151 lines
7.7 KiB
Python

import re, math, sys
from collections import defaultdict
def parse_pcb(path):
txt=open(path).read()
fps=[]
for m in re.finditer(r'\(footprint "([^"]+)"(.*?)\n\t\)\n', txt, re.S):
body=m.group(2)
ref=re.search(r'\(property "Reference" "([^"]+)"', body).group(1)
at=re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)', body)
fx,fy,frot=float(at.group(1)),float(at.group(2)),float(at.group(3) or 0)
layer=re.search(r'\(layer "([^"]+)"\)', body).group(1)
pads=[]
for p in re.finditer(r'\(pad "([^"]*)" (\w+) (\w+)\s*\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)\s*\(size ([\d.]+) ([\d.]+)\)(.*?)\n\t\t\)', body, re.S):
num,ptype,shape=p.group(1),p.group(2),p.group(3)
ox,oy,prot=float(p.group(4)),float(p.group(5)),float(p.group(6) or 0)
sx,sy=float(p.group(7)),float(p.group(8))
rest=p.group(9)
lay=re.search(r'\(layers ([^)]*)\)', rest); lays=re.findall(r'"([^"]+)"', lay.group(1)) if lay else []
net=re.search(r"\(net \"([^\"]*)\"\)", rest)
th=math.radians(frot)
px=fx+ox*math.cos(th)+oy*math.sin(th); py=fy-ox*math.sin(th)+oy*math.cos(th)
# pad absolute rotation = prot (kicad stores absolute rotation in pad 'at' for footprint pads) -> in v8+ pad rotation is relative? In KiCad 8/9 file, pad rotation is absolute (fp rot + pad rot). We'll treat prot as absolute.
pads.append(dict(ref=ref,num=num,type=ptype,shape=shape,x=px,y=py,rot=prot,sx=sx,sy=sy,layers=lays,net=net.group(1) if net else ""))
fps.append(dict(ref=ref,x=fx,y=fy,rot=frot,layer=layer,pads=pads))
segs=[dict(x1=float(a),y1=float(b),x2=float(c),y2=float(d),w=float(w),layer=l,net=n) for a,b,c,d,w,l,n in re.findall(r'\(segment\s*\(start (-?[\d.]+) (-?[\d.]+)\)\s*\(end (-?[\d.]+) (-?[\d.]+)\)\s*\(width ([\d.]+)\)\s*\(layer "([^"]+)"\)\s*\(net "([^"]*)"\)', txt)]
arcs=re.findall(r'\(arc\s*\(start', txt)
vias=[dict(x=float(x),y=float(y),size=float(s),drill=float(d),layers=[l1,l2],net=n) for x,y,s,d,l1,l2,n in re.findall(r'\(via\s*\(at (-?[\d.]+) (-?[\d.]+)\)\s*\(size ([\d.]+)\)\s*\(drill ([\d.]+)\)\s*\(layers "([^"]+)" "([^"]+)"\)(?:\s*\([^)]*\))*?\s*\(net "([^"]*)"\)', txt)]
return fps,segs,vias,len(arcs)
COPPER=["F.Cu","In1.Cu","In2.Cu","B.Cu"]
def pad_layers(p):
out=set()
for l in p["layers"]:
if l=="*.Cu": out.update(COPPER)
elif l.endswith(".Cu"): out.add(l)
return out
def point_in_pad(x,y,p,tol=0.0):
# rotate point into pad frame
th=math.radians(p["rot"]); dx=x-p["x"]; dy=y-p["y"]
u= dx*math.cos(th)-dy*math.sin(th); w= dx*math.sin(th)+dy*math.cos(th)
# kicad y is down; rotation sign: try both conventions by testing symmetric sizes; use |u|<=sx/2 || |w|<=sy/2 with swapped if rot 90
if abs(u)<=p["sx"]/2+tol and abs(w)<=p["sy"]/2+tol: return True
u2= dx*math.cos(th)+dy*math.sin(th); w2=-dx*math.sin(th)+dy*math.cos(th)
return abs(u2)<=p["sx"]/2+tol and abs(w2)<=p["sy"]/2+tol
def seg_dist(px,py,s):
x1,y1,x2,y2=s["x1"],s["y1"],s["x2"],s["y2"]
dx,dy=x2-x1,y2-y1; L2=dx*dx+dy*dy
t=0 if L2==0 else max(0,min(1,((px-x1)*dx+(py-y1)*dy)/L2))
cx,cy=x1+t*dx,y1+t*dy
return math.hypot(px-cx,py-cy)
class UF:
def __init__(s): s.p={}
def f(s,a):
s.p.setdefault(a,a)
while s.p[a]!=a: s.p[a]=s.p[s.p[a]]; a=s.p[a]
return a
def u(s,a,b): s.p[s.f(a)]=s.f(b)
def build(net, fps, segs, vias, exclude_pads=(), exclude_vias=()):
items=[]
for f in fps:
for p in f["pads"]:
if p["net"]==net and (p["ref"],p["num"]) not in exclude_pads: items.append(("pad",p))
for s in segs:
if s["net"]==net: items.append(("seg",s))
for i,v in enumerate(vias):
if v["net"]==net and (round(v["x"],3),round(v["y"],3)) not in exclude_vias: items.append(("via",v))
uf=UF()
n=len(items)
for i in range(n): uf.f(i)
for i in range(n):
ki,oi=items[i]
for j in range(i+1,n):
kj,oj=items[j]
if connected(ki,oi,kj,oj): uf.u(i,j)
return items,uf
def layers_of(k,o):
if k=="pad": return pad_layers(o)
if k=="seg": return {o["layer"]}
if k=="via": return set(COPPER) if o["layers"]==["F.Cu","B.Cu"] else set(o["layers"])
def connected(ka,a,kb,b):
la,lb=layers_of(ka,a),layers_of(kb,b)
if not (la&lb): return False
if ka=="seg" and kb=="seg":
wa,wb=a["w"]/2,b["w"]/2
for (x,y) in ((a["x1"],a["y1"]),(a["x2"],a["y2"])):
if seg_dist(x,y,b)<=wa+wb-0.001+0.002: return True
for (x,y) in ((b["x1"],b["y1"]),(b["x2"],b["y2"])):
if seg_dist(x,y,a)<=wa+wb-0.001+0.002: return True
return False
if ka=="seg" or kb=="seg":
s,o,ko=(a,b,kb) if ka=="seg" else (b,a,ka)
if ko=="via":
return seg_dist(o["x"],o["y"],s)<=s["w"]/2+o["size"]/2-0.001
else:
for (x,y) in ((s["x1"],s["y1"]),(s["x2"],s["y2"])):
if point_in_pad(x,y,o,tol=s["w"]/2*0.0): return True
# also pad center near segment
return seg_dist(o["x"],o["y"],s)<=s["w"]/2+min(o["sx"],o["sy"])/2-0.001
if ka=="via" and kb=="via":
return math.hypot(a["x"]-b["x"],a["y"]-b["y"])<=(a["size"]+b["size"])/2-0.001
if ka=="via" or kb=="via":
v,p=(a,b) if ka=="via" else (b,a)
return point_in_pad(v["x"],v["y"],p,tol=v["size"]/2-0.001)
if ka=="pad" and kb=="pad":
return math.hypot(a["x"]-b["x"],a["y"]-b["y"])<=(min(a["sx"],a["sy"])+min(b["sx"],b["sy"]))/2-0.001
return False
def comp_of(items,uf,key):
for i,(k,o) in enumerate(items):
if k=="pad" and (o["ref"],o["num"])==key: return uf.f(i)
return None
def clip_segments_outside_rect(segs, net, x0,y0,x1,y1):
"""Return segs with the parts inside the rect removed (Liang-Barsky), for a given net."""
out=[]
for s in segs:
if s["net"]!=net: out.append(s); continue
ax,ay,bx,by=s["x1"],s["y1"],s["x2"],s["y2"]
ina = x0<=ax<=x1 and y0<=ay<=y1; inb = x0<=bx<=x1 and y0<=by<=y1
if ina and inb: continue
if not ina and not inb:
# could cross the rect; check with Liang-Barsky
dx,dy=bx-ax,by-ay; t0,t1=0.0,1.0; ok=True
for p,q in ((-dx,ax-x0),(dx,x1-ax),(-dy,ay-y0),(dy,y1-ay)):
if p==0:
if q<0: ok=False; break
else:
t=q/p
if p<0: t0=max(t0,t)
else: t1=min(t1,t)
if ok and t0<t1:
# split into two outside parts
out.append(dict(s,x2=ax+t0*dx,y2=ay+t0*dy)); out.append(dict(s,x1=ax+t1*dx,y1=ay+t1*dy))
else: out.append(s)
continue
# one inside: move inside endpoint to boundary
if ina: ax,ay,bx,by=bx,by,ax,ay # now (ax,ay) outside, (bx,by) inside
dx,dy=bx-ax,by-ay; t=0.0
for p,q in ((-dx,ax-x0),(dx,x1-ax),(-dy,ay-y0),(dy,y1-ay)):
if p<0: t=max(t,q/p)
if t>0.001: out.append(dict(s,x1=ax,y1=ay,x2=ax+t*dx,y2=ay+t*dy))
return out
def kelvin_test(fps,segs,vias,net,pad_ref,pad_num,sense_ref,sense_num):
pad=[p for f in fps for p in f["pads"] if (p["ref"],p["num"])==(pad_ref,pad_num)][0]
x0,y0,x1,y1=pad["x"]-pad["sx"]/2,pad["y"]-pad["sy"]/2,pad["x"]+pad["sx"]/2,pad["y"]+pad["sy"]/2
segs2=clip_segments_outside_rect(segs,net,x0,y0,x1,y1)
vias2=[v for v in vias if not (v["net"]==net and x0<=v["x"]<=x1 and y0<=v["y"]<=y1)]
items,uf=build(net,fps,segs2,vias2,exclude_pads=((pad_ref,pad_num),))
c=comp_of(items,uf,(sense_ref,sense_num))
pads=[o["ref"]+"."+o["num"] for i,(k,o) in enumerate(items) if k=="pad" and uf.f(i)==c]
nvia=sum(1 for i,(k,o) in enumerate(items) if k=="via" and uf.f(i)==c)
return pads,nvia