app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
John Lauer
Add bounded obstacle planning and live calculations using the Adom activity palette
e936bc3
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
"""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)