master
John Lauer Add bounded obstacle planning and live calculations using the Adom activity palette e936bc3 1mo ago
"""Plan from the live obstacle board, then record every verified copper edit."""
import argparse,json,sys,time
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
from call import call
from live_panel import LivePanel
ROOT=Path(__file__).resolve().parent
p=argparse.ArgumentParser();p.add_argument('--document',required=True);p.add_argument('--take',required=True);p.add_argument('--hwnd',type=int,required=True);p.add_argument('--palette-hwnd',type=int,required=True);options=p.parse_args()
TAKE=ROOT/options.take;TAKE.mkdir(exist_ok=False)
log=[]
def invoke(v,a,expected_failure=False):
    start=time.time();r=call(v,{**a,'reason':'Plan and show obstacle-aware routing in Fusion'})
    log.append({'verb':v,'args':a,'start':start,'end':time.time(),'result':r})
    (TAKE/'calls.json').write_text(json.dumps(log,indent=2)+'\n')
    if not expected_failure and not r.get('success',r.get('ok')):raise RuntimeError(json.dumps(r))
    return r
s=invoke('fusion_routing_state',{'expectDocument':options.document})
assert s['bridgeVersion']=='1.10.0-dev.routing.4' and s['segmentCount']==2 and s['viaCount']==0
assert {i['net'] for i in s['segments']}=={'TOP_BARRIER','BOTTOM_BARRIER'}
(TAKE/'before.json').write_text(json.dumps(s,indent=2)+'\n')
def guard():return {'expectDocument':options.document,'boardId':s['boardId'],'expectedRevision':s['revision']}
panel=LivePanel(invoke,options.document,TAKE/'panel-events.jsonl',options.palette_hwnd)
panel.update(create=True,status='Ready to route',connection='3 pad pairs / 2 copper barriers',plan='0.50 mm grid / 0.25 mm traces',checks='Fresh board: 2 existing segments, no vias.')
windows=invoke('desktop_list_windows',{})['windows']
w=next(w for w in windows if w['hwnd']==options.hwnd)
assert not w['minimized'] and w['rect']['width']>=800 and w['rect']['height']>=600,'Fusion window is too small or minimized'
blocked=invoke('fusion_route_net',{**guard(),'net':'ROUTE_1','points':[{'pad':'L1.1'},{'pad':'R1.1'}],'dryRun':True},True)
assert blocked.get('errorCode')=='clearance_violation' and not blocked['mutationAttempted']
invoke('fusion_execute_text_command',{'command':'Electron.ImmediateRepaint /full','expectDocument':options.document})
panel_rec=None
rec=invoke('desktop_record_window_start',{'hwnd':options.hwnd,'fps':30,'audio':False,'maxDurationMs':180000})
(TAKE/'record-start.json').write_text(json.dumps(rec,indent=2)+'\n')
try:
    panel_rec=invoke('desktop_record_window_start',{'hwnd':options.palette_hwnd,'fps':30,'audio':False,'maxDurationMs':180000})
    (TAKE/'panel-record-start.json').write_text(json.dumps(panel_rec,indent=2)+'\n')
    assert panel_rec['width']>=400 and panel_rec['height']>=600
    assert rec['width']>=800 and rec['height']>=600,'Capture dimensions are too small'
    panel.update(status='Direct path rejected safely',connection='ROUTE_1: L1.1 → R1.1',checks='Direct top-layer line crosses existing copper.\nNo mutation attempted.',event='fusion_route_net dryRun: clearance_violation')
    time.sleep(3)
    for n in range(1,4):
        panel.update(percent=(n-1)*33,status=f'Calculating connection {n} of 3',connection=f'ROUTE_{n}: L{n}.1 → R{n}.1\n(5, {8+12*(n-1)}) → (45, {8+12*(n-1)}) mm',plan='0.50 mm grid / 0.25 mm width\n'+('Top only: vias disabled' if n==1 else 'Top + bottom: vias permitted'),checks='Searching live copper obstacles; board unchanged.',event='fusion_plan_route: read-only A* search')
        p=invoke('fusion_plan_route',{**guard(),'net':f'ROUTE_{n}','fromPad':f'L{n}.1','toPad':f'R{n}.1','allowVias':n!=1})
        assert p['revision']==s['revision'] and not p['mutated']
        (TAKE/f'plan-{n}.json').write_text(json.dumps(p,indent=2)+'\n')
        stats=p['plan']['planner']
        plan_text=f"{stats['lengthMm']:g} mm / {stats['viaCount']} vias\n{stats['expandedNodes']} nodes / {stats['elapsedMs']:g} ms\nClearance {p['plan']['clearance']:g} mm"
        panel.update(status='Route calculated and preflight passed',plan=plan_text,checks='Revision unchanged by planning.\nExplicit path cleared against live copper.')
        time.sleep(3)
        for step,i in enumerate(p['plan']['items'],1):
            detail=(f"Layer {i['layer']}: {i['start']} → {i['end']}" if i['kind']=='segment' else f"Via at {i['at']}: top ↔ bottom")
            panel.update(status=f"Creating {i['kind']} {step}/{len(p['plan']['items'])}",checks=detail+'\nWaiting for native copper read-back.',event='fusion_route_net: '+i['kind'])
            if i['kind']=='segment':a={'net':i['net'],'width':i['width'],'layer':i['layer'],'points':[i['start'],i['end']]}
            else:
                xy=i['at'];a={'net':i['net'],'viaDiameter':i['diameter'],'viaDrill':i['drill'],'points':[xy,{'x':xy[0],'y':xy[1],'layer':16}]}
            s=invoke('fusion_route_net',{**guard(),**a});assert s['verified'] and s['mutated']
            panel.update(status=f"Verified {i['kind']} {step}/{len(p['plan']['items'])}",checks='Native copper matches requested edit.\nRevision advanced; existing copper preserved.')
            time.sleep(.8)
        panel.update(status='Running native Autodesk DRC',checks='Measuring errors, warnings and airwires.',event='fusion_routing_drc')
        s=invoke('fusion_routing_drc',guard());assert s['drc']['checked'] and not s['drc']['errorCount'] and not s['drc']['warningCount'],s['drc']
        panel.update(percent=n*33,status=f'Connection {n} verified',checks=f"DRC: {s['drc']['errorCount']} errors / {s['drc']['warningCount']} warnings\n{s['drc'].get('airwireCount','?')} airwires / {len(s['remainingNets'])} nets remaining",event='Native DRC completed')
        time.sleep(2)
        print(n,p['plan']['planner'],len(s['remainingNets']),'remaining',flush=True)
    assert not s['remainingNets'] and s['drc']['clean']
    assert [i for i in s['segments'] if i['net'] in {'TOP_BARRIER','BOTTOM_BARRIER'}]==json.loads((TAKE/'before.json').read_text())['segments']
    (TAKE/'final.json').write_text(json.dumps(s,indent=2)+'\n');panel.update(percent=100,status='All 3 connections complete',connection='6 pads connected / 8 new segments / 2 vias',checks='Native DRC: 0 errors / 0 warnings / 0 airwires\nBoth original copper barriers unchanged.',event='Final board geometry verified');time.sleep(5)
except Exception as exc:
    try:panel.update(status='STOPPED: operation failed',checks=str(exc)[:400],failed=True,event='No automatic mutation retry')
    except Exception:pass
    raise
finally:
    if panel_rec:
        panel_stop=invoke('desktop_record_window_stop',{'recordingId':panel_rec['recordingId']},True)
        (TAKE/'panel-record-stop.json').write_text(json.dumps(panel_stop,indent=2)+'\n')
    stop=invoke('desktop_record_window_stop',{'recordingId':rec['recordingId']},True)
    (TAKE/'record-stop.json').write_text(json.dumps(stop,indent=2)+'\n')
assert stop.get('success',stop.get('ok')) and stop.get('bytes',0)>0,stop
invoke('pull_file',{'filePaths':[stop['path'],panel_stop['path']],'saveTo':str(TAKE)})
print('PASS: live obstacle planning, copper preserved, native DRC clean',flush=True)