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-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
#!/usr/bin/env python3
"""kicad_board_map.py - render geom.json (from kicad_board_geom.py) as an SVG board map:
outline, copper, silkscreen, vias, pads coloured by functional section.
Usage: python3 kicad_board_map.py <geom.json> <F|B> <out.svg> [blocks.json]
blocks.json: {"section-id": ["R1","U2",...], ...} - optional; palette assigned in key order.
"""
import json,math,sys,itertools
G=json.load(open(sys.argv[1]))
side=sys.argv[2] if len(sys.argv)>2 else 'F'
out=sys.argv[3] if len(sys.argv)>3 else f'board-{side}.svg'
B=json.load(open(sys.argv[4])) if len(sys.argv)>4 else {}
BLK={r:k for k,v in B.items() for r in v}
PALETTE=['#f5a524','#f97316','#3b82f6','#a855f7','#06b6d4','#22c55e','#ef4444','#64748b',
'#eab308','#14b8a6','#8b5cf6','#f43f5e']
COL={k:PALETTE[i%len(PALETTE)] for i,k in enumerate(B.keys())}
mode='map' if B else 'bare'
xs=[];ys=[]
for k,d in G['edges']:
if k=='L': xs+=[d[0],d[2]];ys+=[d[1],d[3]]
elif k=='A': xs+=[d[0],d[2],d[4]];ys+=[d[1],d[3],d[5]]
elif k=='R': xs+=[d[0],d[2]];ys+=[d[1],d[3]]
elif k=='P': xs+=[p[0] for p in d];ys+=[p[1] for p in d]
X0,X1,Y0,Y1=min(xs),max(xs),min(ys),max(ys)
PAD=3; W=X1-X0+2*PAD; H=Y1-Y0+2*PAD
S=8 # px per mm
mir = (side=='B')
def T(x,y):
X = (X1-x) if mir else (x-X0)
return ((X+PAD)*S, (y-Y0+PAD)*S)
o=[]
o.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W*S:.1f} {H*S:.1f}" width="{W*S:.0f}" height="{H*S:.0f}" font-family="ui-sans-serif,system-ui,sans-serif">')
o.append('<defs><linearGradient id="fr4" x1="0" y1="0" x2="0" y2="1">'
'<stop offset="0" stop-color="#0f3d2e"/><stop offset="1" stop-color="#0a2d22"/></linearGradient>'
'<filter id="sh" x="-20%" y="-20%" width="140%" height="140%">'
'<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#000" flood-opacity=".45"/></filter></defs>')
# --- board outline path
def edge_path():
segs=[]
for k,d in G['edges']:
if k=='L': segs.append([(d[0],d[1]),(d[2],d[3])])
elif k=='A':
ax,ay,mx,my,bx,by=d
dd=2*(ax*(my-by)+mx*(by-ay)+bx*(ay-my))
if abs(dd)<1e-9: segs.append([(ax,ay),(bx,by)]); continue
ux=((ax*ax+ay*ay)*(my-by)+(mx*mx+my*my)*(by-ay)+(bx*bx+by*by)*(ay-my))/dd
uy=((ax*ax+ay*ay)*(bx-mx)+(mx*mx+my*my)*(ax-bx)+(bx*bx+by*by)*(mx-ax))/dd
r=math.hypot(ax-ux,ay-uy)
a1=math.atan2(ay-uy,ax-ux);am=math.atan2(my-uy,mx-ux);a2=math.atan2(by-uy,bx-ux)
while am-a1>math.pi: am-=2*math.pi
while am-a1<-math.pi: am+=2*math.pi
while a2-am>math.pi: a2-=2*math.pi
while a2-am<-math.pi: a2+=2*math.pi
N=14; segs.append([(ux+r*math.cos(a1+(a2-a1)*t/N), uy+r*math.sin(a1+(a2-a1)*t/N)) for t in range(N+1)])
elif k=='R':
x0,y0,x1,y1=d; segs.append([(x0,y0),(x1,y0),(x1,y1),(x0,y1),(x0,y0)])
elif k=='P': segs.append([tuple(p) for p in d]+[tuple(d[0])])
# chain segments into a loop
used=[False]*len(segs); chain=list(segs[0]); used[0]=True
for _ in range(len(segs)):
best=None;bd=1e9
for i,sg in enumerate(segs):
if used[i]: continue
for endp,pts in ((sg[0],sg),(sg[-1],list(reversed(sg)))):
dd=math.dist(chain[-1],endp)
if dd<bd: bd=dd;best=(i,pts)
if best is None or bd>0.6: break
used[best[0]]=True; chain+=best[1][1:]
return 'M'+' L'.join(f'{T(*p)[0]:.2f},{T(*p)[1]:.2f}' for p in chain)+' Z'
EP=edge_path()
o.append(f'<path d="{EP}" fill="url(#fr4)" stroke="#134e37" stroke-width="1.2" filter="url(#sh)"/>')
o.append(f'<clipPath id="brd"><path d="{EP}"/></clipPath><g clip-path="url(#brd)">')
# --- tracks
tl = 'F.Cu' if side=='F' else 'B.Cu'
for (d,w,L) in G['tracks']:
if L!=tl: continue
a=T(d[0],d[1]);b=T(d[2],d[3])
o.append(f'<line x1="{a[0]:.2f}" y1="{a[1]:.2f}" x2="{b[0]:.2f}" y2="{b[1]:.2f}" stroke="#b78227" stroke-opacity=".55" stroke-width="{w*S:.2f}" stroke-linecap="round"/>')
for v in G['vias']:
p=T(v[0],v[1]); o.append(f'<circle cx="{p[0]:.2f}" cy="{p[1]:.2f}" r="{v[2]*S:.2f}" fill="#8a6420" fill-opacity=".6"/>')
o.append('</g>')
# --- block wash
if mode=='map':
import collections
bb=collections.defaultdict(lambda:[1e9,1e9,-1e9,-1e9])
for f in G['fps']:
k=BLK.get(f['ref'])
if not k: continue
for pd in f['pads']:
r=max(pd['w'],pd['h'])/2
b=bb[k]
b[0]=min(b[0],pd['x']-r);b[1]=min(b[1],pd['y']-r);b[2]=max(b[2],pd['x']+r);b[3]=max(b[3],pd['y']+r)
# --- pads
for f in G['fps']:
if f['layer']!=('F.Cu' if side=='F' else 'B.Cu') and not any(pd['drill'] for pd in f['pads']): continue
k=BLK.get(f['ref']); c=COL.get(k,'#c9a227')
for pd in f['pads']:
if not (tl in pd['layers'] or '*.Cu' in pd['layers']): continue
x,y=T(pd['x'],pd['y']); w,h=pd['w']*S,pd['h']*S
rot=-pd['rot'] if not mir else pd['rot']
rx = min(w,h)*0.22 if pd['shape'] in('roundrect','rect') else min(w,h)/2
if pd['shape']=='circle': rx=min(w,h)/2
o.append(f'<rect x="{-w/2:.2f}" y="{-h/2:.2f}" width="{w:.2f}" height="{h:.2f}" rx="{rx:.2f}" '
f'fill="{c}" fill-opacity="{0.95 if mode=="map" else 0.85}" transform="translate({x:.2f},{y:.2f}) rotate({rot:.1f})"/>')
# --- silkscreen
sl='F.SilkS' if side=='F' else 'B.SilkS'
o.append('<g stroke="#e9eef2" stroke-opacity=".7" fill="none" stroke-width="0.9" stroke-linecap="round">')
for f in G['fps']:
for k,d in f['silk']:
if k=='L':
a=T(d[0],d[1]);b=T(d[2],d[3]); o.append(f'<path d="M{a[0]:.2f},{a[1]:.2f}L{b[0]:.2f},{b[1]:.2f}"/>')
elif k=='P':
pts=' '.join(f'{T(*p)[0]:.2f},{T(*p)[1]:.2f}' for p in d); o.append(f'<polygon points="{pts}" fill="#e9eef2" fill-opacity=".5" stroke="none"/>')
elif k=='C':
p=T(d[0],d[1]); o.append(f'<circle cx="{p[0]:.2f}" cy="{p[1]:.2f}" r="{d[2]*S:.2f}"/>')
elif k=='A':
a=T(d[0],d[1]);m=T(d[2],d[3]);b=T(d[4],d[5])
o.append(f'<path d="M{a[0]:.2f},{a[1]:.2f}Q{m[0]:.2f},{m[1]:.2f} {b[0]:.2f},{b[1]:.2f}"/>')
o.append('</g>')
# --- refdes labels for the map
if mode=='map':
o.append('<g font-size="7" text-anchor="middle" fill="#eaf2ee" font-weight="600">')
BIG={'U2','U1','U3','U4','U5','U6','J1','J2','J3','J6','J16','J23','SW1','SW2','SW3','Y1','F1','F2','D15','D16','C9'}
for f in G['fps']:
if f['ref'] in BIG or f['ref'].startswith('J') and f['ref'][1:].isdigit() and 7<=int(f['ref'][1:])<=13:
x,y=T(f['x'],f['y']); o.append(f'<text x="{x:.1f}" y="{y-6:.1f}" stroke="#07231a" stroke-width="2" paint-order="stroke">{f["ref"]}</text>')
o.append('</g>')
o.append('</svg>')
open(out,'w').write('\n'.join(o))
print('wrote',out, f'{W*S:.0f}x{H*S:.0f}px ({X1-X0:.1f}x{Y1-Y0:.1f} mm)')