skill
Publish a Hardware Component to the Adom Wiki
Public Made by Adomby adom
The one-stop-shop for publishing a hardware component (or molecule/board) to the Adom Wiki. The whole lifecycle: page anatomy, the clone→edit→preview→push loop and its hard rules, making a great hero image (Hero Component Studio — 16:10, overlay-safe, datasheet-backed), the animated 3D composite, the interactive 3D viewer, ct thumbnails, the datasheet-accuracy audit, and every wiki push/pull gotcha we've hit. Now includes the worked Path B (board) pipeline: STEP-to-GLB, refdes tagging, layer de-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
#!/usr/bin/env python3
"""kicad_board_geom.py - parse a .kicad_pcb into geom.json (outline, footprints+pads with
transforms applied, silkscreen, tracks, vias). Stdlib-only s-expression parse; no KiCad needed.
Feeds kicad_board_map.py (SVG board maps) and the Path B README builder.
Usage: python3 kicad_board_geom.py <board.kicad_pcb> [out_geom.json]
"""
import re,json,math,sys
SRC=sys.argv[1]
OUT=sys.argv[2] if len(sys.argv)>2 else 'geom.json'
s=open(SRC,encoding='utf8',errors='replace').read()
def toks(txt,i):
"""return (list, next_index) parsing one s-expr starting at txt[i]=='('"""
assert txt[i]=='('
out=[];i+=1;cur=''
while i<len(txt):
c=txt[i]
if c=='(':
sub,i=toks(txt,i);
if cur: out.append(cur); cur=''
out.append(sub); continue
if c==')':
if cur: out.append(cur)
return out,i+1
if c=='"':
j=i+1;buf=''
while txt[j]!='"' or txt[j-1]=='\\':
buf+=txt[j];j+=1
if cur: out.append(cur);cur=''
out.append(('STR',buf)); i=j+1; continue
if c.isspace():
if cur: out.append(cur);cur=''
i+=1;continue
cur+=c;i+=1
return out,i
def find_all(node,name):
for c in node:
if isinstance(c,list) and c and c[0]==name: yield c
def get(node,name):
for c in find_all(node,name): return c
return None
def sval(x): return x[1] if isinstance(x,tuple) else x
def nums(node,n=2,start=1):
return [float(sval(v)) for v in node[start:start+n]]
print('parsing…',file=sys.stderr)
root,_=toks(s,s.index('(kicad_pcb'))
print('top-level children:',len(root),file=sys.stderr)
def xf(px,py,ox,oy,rot):
a=math.radians(rot)
return (ox+px*math.cos(a)+py*math.sin(a), oy-px*math.sin(a)+py*math.cos(a))
edges=[];silk=[];pads=[];fabtext=[];courts=[]
def layer_of(n):
l=get(n,'layer')
return sval(l[1]) if l else ''
def layers_of(n):
l=get(n,'layers')
return [sval(v) for v in l[1:]] if l else []
def arc_pts(x1,y1,xm,ym,x2,y2,steps=16):
# circle through 3 points
ax,ay,bx,by,cx,cy=x1,y1,xm,ym,x2,y2
d=2*(ax*(by-cy)+bx*(cy-ay)+cx*(ay-by))
if abs(d)<1e-9: return [(x1,y1),(x2,y2)]
ux=((ax*ax+ay*ay)*(by-cy)+(bx*bx+by*by)*(cy-ay)+(cx*cx+cy*cy)*(ay-by))/d
uy=((ax*ax+ay*ay)*(cx-bx)+(bx*bx+by*by)*(ax-cx)+(cx*cx+cy*cy)*(bx-ax))/d
r=math.hypot(ax-ux,ay-uy)
a1=math.atan2(ay-uy,ax-ux);am=math.atan2(by-uy,bx-ux);a2=math.atan2(cy-uy,cx-ux)
def norm(a,b):
while b-a>math.pi: b-=2*math.pi
while b-a<-math.pi: b+=2*math.pi
return b
am=norm(a1,am); a2=norm(am,a2)
return [(ux+r*math.cos(a1+(a2-a1)*t/steps), uy+r*math.sin(a1+(a2-a1)*t/steps)) for t in range(steps+1)]
# ---- board graphics
for n in root:
if not isinstance(n,list) or not n: continue
t=n[0]
if t in ('gr_line','gr_arc','gr_rect','gr_circle','gr_poly'):
if layer_of(n)!='Edge.Cuts': continue
if t=='gr_line': edges.append(('L',nums(get(n,'start'))+nums(get(n,'end'))))
elif t=='gr_arc': edges.append(('A',nums(get(n,'start'))+nums(get(n,'mid'))+nums(get(n,'end'))))
elif t=='gr_rect': edges.append(('R',nums(get(n,'start'))+nums(get(n,'end'))))
elif t=='gr_circle': edges.append(('C',nums(get(n,'center'))+nums(get(n,'end'))))
elif t=='gr_poly':
pts=get(n,'pts'); edges.append(('P',[nums(p) for p in find_all(pts,'xy')]))
fps=[]
for n in find_all(root,'footprint'):
at=get(n,'at'); ox,oy=nums(at); rot=float(sval(at[3])) if len(at)>3 else 0.0
ref=''
for p in find_all(n,'property'):
if sval(p[1])=='Reference': ref=sval(p[2])
lay=layer_of(n)
fp={'ref':ref,'x':ox,'y':oy,'rot':rot,'layer':lay,'pads':[],'silk':[],'court':[]}
for pd in find_all(n,'pad'):
pat=get(pd,'at'); px,py=nums(pat); prot=float(sval(pat[3])) if len(pat)>3 else 0.0
sz=get(pd,'size'); w,h=nums(sz) if sz else (0.5,0.5)
shape=sval(pd[3]) if len(pd)>3 else 'rect'
lys=layers_of(pd)
gx,gy=xf(px,py,ox,oy,rot)
fp['pads'].append({'x':gx,'y':gy,'w':w,'h':h,'rot':rot+prot,'shape':shape,'layers':lys,
'drill':bool(get(pd,'drill'))})
for g in n:
if not isinstance(g,list) or not g: continue
gt=g[0]
if gt in ('fp_line','fp_arc','fp_rect','fp_circle','fp_poly'):
L=layer_of(g)
tgt = fp['silk'] if L in ('F.SilkS','B.SilkS') else (fp['court'] if L in ('F.CrtYd','B.CrtYd') else None)
if tgt is None: continue
if gt=='fp_line':
a=nums(get(g,'start'));b=nums(get(g,'end'))
tgt.append(('L',[*xf(*a,ox,oy,rot),*xf(*b,ox,oy,rot)]))
elif gt=='fp_arc':
a=nums(get(g,'start'));m=nums(get(g,'mid'));b=nums(get(g,'end'))
tgt.append(('A',[*xf(*a,ox,oy,rot),*xf(*m,ox,oy,rot),*xf(*b,ox,oy,rot)]))
elif gt=='fp_rect':
a=nums(get(g,'start'));b=nums(get(g,'end'))
c=[(a[0],a[1]),(b[0],a[1]),(b[0],b[1]),(a[0],b[1])]
tgt.append(('P',[xf(*p,ox,oy,rot) for p in c]))
elif gt=='fp_circle':
c=nums(get(g,'center'));e=nums(get(g,'end'))
tgt.append(('C',[*xf(*c,ox,oy,rot),math.dist(c,e)]))
elif gt=='fp_poly':
pts=get(g,'pts'); tgt.append(('P',[xf(*nums(p),ox,oy,rot) for p in find_all(pts,'xy')]))
fps.append(fp)
# ---- tracks
tracks=[]
for n in find_all(root,'segment'):
tracks.append((nums(get(n,'start'))+nums(get(n,'end')), float(sval(get(n,'width')[1])), layer_of(n)))
vias=[]
for n in find_all(root,'via'):
vias.append(nums(get(n,'at'))+[float(sval(get(n,'size')[1]))/2])
print(f'edges={len(edges)} fps={len(fps)} tracks={len(tracks)} vias={len(vias)}',file=sys.stderr)
json.dump({'edges':edges,'fps':fps,'tracks':tracks,'vias':vias},open(OUT,'w'))
print('wrote',OUT)