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
Preserve BQ independent routing, narration and native copper-retention evidence
a4b7711
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
"""Executed on Fusion's main thread: explicit validated geometry, visible per primitive.
REPLAY_ROOT and PLAN_CONTEXT are supplied by the guarded driver. Never calls AUTO.
"""
import json,time,math,re,urllib.request
from pathlib import Path
import adsk
exec((Path(REPLAY_ROOT)/'routing_script.py').read_text())
plan_data=json.loads((Path(REPLAY_ROOT)/'candidate-plan.json').read_text())
args=PLAN_CONTEXT
root,board,meta=snapshot(app,args)
assert meta['revision']==args['expectedRevision'] and meta['boardId']==args['boardId']
assert not any(s.findall('polygonpour') or s.findall('polygon') for s in board.findall('signals/signal'))
start_route=args.get('startRoute',0);start_primitive=args.get('startPrimitive',0)
if start_route==0 and start_primitive==0:
assert not objects(board)[2] and not objects(board)[3]
base=native_drc(app);baseline={e['signature'] for e in base['errors'] if e['code']!=21};assert base['errorCount']==1
else:baseline=set(args['baselineSignatures'])
net_names={s.get('name') for s in board.findall('signals/signal')}
items=[];routes=[]
for r in plan_data['routes']:
n=r['net'];assert n in net_names and re.fullmatch(r'[A-Za-z0-9_+!./:$-]{1,128}',n)
batch=[]
for p in r['parts']:
assert p['layer'] in [1,2,15,16]
for a,b in zip(p['points'],p['points'][1:]):
if math.dist(a,b)<1e-7:continue
batch.append({'kind':'segment','net':n,'layer':p['layer'],'width':p['width'],'start':[round(v,6) for v in a],'end':[round(v,6) for v in b]})
for at in r['vias']:batch.append({'kind':'via','net':n,'at':[round(v,6) for v in at],'drill':.2,'diameter':.45,'extent':'1-16'})
routes.append((n,batch));items+=batch
offsets=[0]
for _,batch in routes:offsets.append(offsets[-1]+len(batch))
if 'startPrimitive' not in args:start_primitive=offsets[start_route]
stop_primitive=min(start_primitive+args.get('maxPrimitives',80),args.get('endPrimitive',len(items)),len(items))
prefix=items[:start_primitive]
before_objects=objects(board);assert copper_matches(prefix,before_objects[2]+before_objects[3])
logpath=Path(REPLAY_ROOT)/'live-events.jsonl';completed=len(prefix);started=time.time()
def panel(title,connection,plan,checks,percent):
p=app.userInterface.palettes.itemById('AdomActivityPalette')
assert p and p.isVisible,'Existing Adom palette closed; stop before next edit'
payload={'title':'Astra: '+title,'detail':'Codex Astra in Adom Hydrogen / explicit four-layer routing','percent':percent,'tone':'done' if percent==100 else 'working','routing':{'connection':connection,'plan':plan,'checks':checks,'events':'No original traces, vias or pours used by planner'}}
req=urllib.request.Request('http://127.0.0.1:8893/state',data=json.dumps(payload).encode(),headers={'Content-Type':'application/json'})
seq=json.load(urllib.request.urlopen(req,timeout=3))['sequence']
for _ in range(60):
adsk.doEvents()
status=json.load(urllib.request.urlopen('http://127.0.0.1:8893/state',timeout=2))
if status['ack']>=seq:break
time.sleep(.05)
else:raise RuntimeError('Live palette did not acknowledge the update')
with logpath.open('a') as f:f.write(json.dumps({'time':time.time(),'completed':completed,'sequence':seq,'payload':payload})+'\n')
if start_primitive==0:
panel('Starting from zero copper','50 nets / 304 pads / 109 components','Validated candidate / 4 copper layers','0 traces / 0 vias / 0 old pours\nBaseline: 1 placement error',0)
app.executeTextCommand('Electron.run DISPLAY NONE; DISPLAY 1 2 15 16 17 18 20 21; WINDOW (-1mm -1mm) (52mm 28mm);')
bounded=False
for index,(net,full_batch) in enumerate(routes):
batch=full_batch[max(0,start_primitive-offsets[index]):min(len(full_batch),stop_primitive-offsets[index])]
if offsets[index]>=stop_primitive or offsets[index+1]<=start_primitive or not batch:continue
editor_identity(app,args['expectDocument'])
length=sum(math.dist(i['start'],i['end']) for i in batch if i['kind']=='segment')
layers=sorted({i['layer'] for i in batch if i['kind']=='segment'})
panel('Creating '+net,net+' / connection '+str(index+1)+' of '+str(len(routes)),f'{length:.2f} mm / layers {layers}\n{len(batch)} explicit copper primitives',f'{completed} / {len(items)} primitives placed\nFinal native check follows',round(completed*95/len(items)))
for item in batch:
if time.time()-started>25:bounded=True;break
if item['kind']=='segment':
command="LAYER %d; LINE '%s' %.6fmm (%.6fmm %.6fmm) +0 (%.6fmm %.6fmm);"%(item['layer'],net,item['width'],*item['start'],*item['end'])
else:command="CHANGE DRILL %.6fmm; VIA '%s' %.6fmm ROUND 1-16 (%.6fmm %.6fmm);"%(item['drill'],net,item['diameter'],*item['at'])
app.executeTextCommand('Electron.run '+command);app.executeTextCommand('Electron.ImmediateRepaint /full');adsk.doEvents();completed+=1;time.sleep(.055)
(Path(REPLAY_ROOT)/'live-progress.json').write_text(json.dumps({'completedPrimitives':completed,'route':index,'time':time.time()}))
if bounded:break
app.executeTextCommand('Electron.run RATSNEST;')
panel('Checking native copper','Explicit geometry checkpoint','Comparing native copper with the calculated plan','Exact copper read-back; final DRC follows all routes',round(completed*95/len(items)))
_,after,aftermeta=snapshot(app,args);objs=objects(after)
expected_items=items[:completed];verified=copper_matches(expected_items,objs[2]+objs[3])
final=completed==len(items);report=native_drc(app) if final else None
# RATSNEST and the copper snapshot above already measured connectivity; DRC adds
# an error report without changing the copper. Avoid a redundant full export.
new_errors=[e for e in report['errors'] if e['code']!=21 and e['signature'] not in baseline] if final else []
result={'success':verified and (not final or report['airwireCount']==0 and not new_errors),'final':final,'nextPrimitive':completed,'baselineSignatures':sorted(baseline),'verified':verified,'completedPrimitives':completed,'elapsed':time.time()-started,'newErrors':new_errors,**state(after,aftermeta)}
if final:result['drc']=report
(Path(REPLAY_ROOT)/'live-result.json').write_text(json.dumps(result,indent=2))
assert result['success'],'Native result differs or introduces errors; retain take for diagnosis'
if final:panel('All 50 nets connected','Independent Astra routing complete',f"{result['segmentCount']} segments / {result['viaCount']} vias / 0 pours",'0 airwires / 0 warnings / 0 new DRC errors\n1 unchanged placement-clearance error',100)