main
ae007 Project page: AFS_Adom rev A (Rice Eclipse AFS clean-room AI redesign): highlight reel and narrated process video with… cbe460e 7d ago
"""Routing build-up animation for the project reel, drawn from the final AFS_Adom.kicad_pcb: board outline and pads,
then every track segment and via in file order (the fixed QFN fan-out first, then the router's 415 traces as landed),
then the filled pours fading in per layer. Frames -> ffmpeg. PIL only; geometry straight from the file."""
import math, re, sys, subprocess, os
from PIL import Image, ImageDraw, ImageFont
sys.path.insert(0, os.path.dirname(__file__))
from silk import parse_sexp, kids, kid
S = '/tmp/claude-1001/-home-adom-project/8819fb5f-c2c6-4524-9528-99428cd6b88e/scratchpad'
OUT = S + '/reel/buildup'; os.makedirs(OUT, exist_ok=True)
W, H = 1920, 1080; X0, Y0, BW, BH = 100.0, 100.0, 92.0, 68.0
SC = min((W - 260) / BW, (H - 250) / BH); OX = (W - BW * SC) / 2; OY = 170
def P(x, y): return (OX + (x - X0) * SC, OY + (y - Y0) * SC)
COL = {'F.Cu': (232, 72, 60), 'B.Cu': (70, 140, 240), 'In1.Cu': (200, 170, 60), 'In2.Cu': (170, 90, 200)}
tree = parse_sexp(open(S + '/AFS_Adom/AFS_Adom.kicad_pcb', encoding='utf-8').read())[0]
segs = []; vias = []; zones = []; pads = []
for n in tree[1:]:
    if not isinstance(n, list): continue
    if n[0] == 'segment':
        s, e = kid(n, 'start'), kid(n, 'end')
        segs.append(('seg', (float(s[1]), float(s[2])), (float(e[1]), float(e[2])), float(kid(n, 'width')[1]), kid(n, 'layer')[1]))
    elif n[0] == 'via':
        a = kid(n, 'at'); segs.append(('via', (float(a[1]), float(a[2])), None, float(kid(n, 'size')[1]), None))
    elif n[0] == 'zone':
        for fp in kids(n, 'filled_polygon'):
            lay = kid(fp, 'layer')[1]; pts = [(float(xy[1]), float(xy[2])) for xy in kids(kid(fp, 'pts'), 'xy')]
            if len(pts) > 2: zones.append((lay, pts))
    elif n[0] == 'footprint':
        at = kid(n, 'at'); cx, cy = float(at[1]), float(at[2]); rot = float(at[3]) if len(at) > 3 else 0
        for p in kids(n, 'pad'):
            pa = kid(p, 'at'); px, py = float(pa[1]), float(pa[2]); c, s_ = math.cos(math.radians(rot)), math.sin(math.radians(rot))
            ax, ay = cx + px * c + py * s_, cy - px * s_ + py * c; sz = kid(p, 'size'); w, h = float(sz[1]), float(sz[2])
            pang = float(pa[3]) if len(pa) > 3 else rot
            if (round(pang) // 90) % 2: w, h = h, w
            pads.append((ax, ay, w, h, 'B.Cu' if any('B.Cu' == l for l in kid(p, 'layers')[1:]) and not any(l in ('F.Cu', '*.Cu') for l in kid(p, 'layers')[1:]) else 'F.Cu'))
print('segments+vias', len(segs), 'zone polys', len(zones), 'pads', len(pads))
try:
    FB = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 44); FS = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 28)
except Exception:
    FB = FS = ImageFont.load_default()
base = Image.new('RGB', (W, H), (12, 14, 20)); d = ImageDraw.Draw(base)
d.rounded_rectangle([*P(X0, Y0), *P(X0 + BW, Y0 + BH)], radius=2 * SC, fill=(18, 52, 32), outline=(190, 200, 190), width=3)
for ax, ay, w, h, lay in pads:
    a = P(ax - w / 2, ay - h / 2); b = P(ax + w / 2, ay + h / 2); d.rectangle([a, b], fill=(214, 168, 70))
FPS = 30; T_ROUTE = 11.0; T_POUR = 3.0; T_HOLD = 1.5
nframes = int((T_ROUTE + T_POUR + T_HOLD) * FPS); per = math.ceil(len(segs) / (T_ROUTE * FPS))
canvas = base.copy(); cd = ImageDraw.Draw(canvas); k = 0
def caption(img, t1, t2):
    dd = ImageDraw.Draw(img); dd.text((60, 40), t1, font=FB, fill=(240, 240, 240)); dd.text((60, 100), t2, font=FS, fill=(170, 180, 190))
for f in range(nframes):
    t = f / FPS
    if t < T_ROUTE:
        for item in segs[k:k + per]:
            if item[0] == 'seg':
                _, a, b, w, lay = item; cd.line([P(*a), P(*b)], fill=COL.get(lay, (200, 200, 200)), width=max(2, int(w * SC)))
            else:
                _, a, _, sz, _ = item; r = sz / 2 * SC; x, y = P(*a); cd.ellipse([x - r, y - r, x + r, y + r], fill=(235, 235, 235))
        k += per
        frame = canvas.copy(); caption(frame, 'Routing lands, one trace at a time', f'{min(k, len(segs))} of {len(segs)} tracks and vias  |  red F.Cu, blue B.Cu')
    else:
        frame = canvas.copy()
        a = min(1.0, (t - T_ROUTE) / T_POUR)
        ov = Image.new('RGBA', (W, H), (0, 0, 0, 0)); od = ImageDraw.Draw(ov)
        for lay, pts in zones:
            if lay in ('F.Cu', 'B.Cu'):
                c = COL[lay]; od.polygon([P(*p) for p in pts], fill=(c[0], c[1], c[2], int(70 * a)))
        frame = Image.alpha_composite(frame.convert('RGBA'), ov).convert('RGB')
        caption(frame, 'Pours fill: GND, servo rail, VSYS, 5 V, 3.3 V', '4 layers  |  92 x 68 mm Adom molecule  |  0 DRC errors, 0 unconnected')
    frame.save(f'{OUT}/f{f:05d}.png')
subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-framerate', str(FPS), '-i', f'{OUT}/f%05d.png', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '20', S + '/reel/seg1_buildup.mp4'], check=True)
print('done', nframes, 'frames')