151 lines
7.7 KiB
Python
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
|