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
import json,time,sys
from pathlib import Path
from native import execute,P
from call import call
sys.path.insert(0,str(P.parents[1]));from live_panel import LivePanel
DOC='bq25792-ablation-live-02';TAKE=P/'take-02';TAKE.mkdir(exist_ok=False);logs=[];stages=[]
def invoke(v,a):
start=time.time();r=call(v,a);logs.append({'verb':v,'args':a,'start':start,'end':time.time(),'result':r});(TAKE/'calls.json').write_text(json.dumps(logs,indent=2));assert r.get('success'),r;return r
def native(body):
start=time.time();r=execute(DOC,body);logs.append({'verb':'fusion_run_modeling_script','start':start,'end':time.time(),'result':r});(TAKE/'calls.json').write_text(json.dumps(logs,indent=2));return r
def command(cmd):return native("app.executeTextCommand("+repr('Electron.run '+cmd)+")\napp.executeTextCommand('Electron.ImmediateRepaint /full')\nresult={'commandCompleted':True}")
w=invoke('desktop_list_windows',{})['windows'];main=next(x for x in w if x.get('image')=='Fusion360.exe' and x.get('owner')==0)
class CaptionLog:
def update(self,**values):
with (TAKE/'caption-events.jsonl').open('a') as f:f.write(json.dumps({'time':time.time(),**values})+'\n')
panel=CaptionLog()
s=native("result=handle(app,{'expectDocument':target.name},'routing_state')");assert s['pourCount']==0 and s['segmentCount']==612 and s['viaCount']==60
records=[]
try:
for name,hwnd in [('board',main['hwnd'])]:
r=invoke('desktop_record_window_start',{'hwnd':hwnd,'fps':30,'audio':False,'maxDurationMs':360000,'reason':'Record native copper pours, floating islands and ablation-area validation'});records.append((name,r));(TAKE/(name+'-start.json')).write_text(json.dumps(r,indent=2))
def stage(name):stages.append({'name':name,'time':time.time()});(TAKE/'stages.json').write_text(json.dumps(stages,indent=2))
stage('trace-only');time.sleep(4)
for i,l in enumerate([1,2,15,16]):
stage('pour-'+str(l));panel.update(status='Fill copper layer '+str(l),connection='PGND-connected fill plus floating orphans',plan='Solid pour / 0.1 mm minimum isolation / 0.2 mm edge clearance',checks='Native refill respects board rules and keepouts',event='Create a full-board polygon natively',percent=i*20)
cmd=f"DISPLAY NONE; DISPLAY {l} 17 18 20; LAYER {l}; POLYGON 'PGND' 0.1mm (0mm 0mm) +0 (50.4mm 0mm) +0 (50.4mm 26.4mm) +0 (0mm 26.4mm) +0 (0mm 0mm); CHANGE ISOLATE 0.1mm (25mm 0mm); CHANGE THERMALS OFF (25mm 0mm); CHANGE ORPHANS ON (25mm 0mm); CHANGE RANK 6 (25mm 0mm); RATSNEST; WINDOW (-1mm -1mm) (52mm 28mm);"
command(cmd);stage('filled-'+str(l));time.sleep(4)
stage('orphans-off');panel.update(status='Compare floating islands',connection='Top layer: orphan retention temporarily OFF',plan='Disconnected copper is removed by ordinary pour cleanup',checks='For ablation, removing this copper adds work',event='Compare before and after retaining islands',percent=85);command('DISPLAY NONE; DISPLAY 1 17 18 20; LAYER 1; CHANGE ORPHANS OFF (25mm 0mm); RATSNEST; WINDOW (20mm 7mm) (39mm 18mm);');stage('orphans-off-visible');time.sleep(4)
stage('orphans-on');panel.update(status='Keep isolated copper',connection='Top layer: orphan retention ON',plan='Retain electrically floating copper where clearances allow',checks='Do not place floating islands inside isolation barriers',event='Restore copper-retention policy',percent=90);command('CHANGE ORPHANS ON (25mm 0mm); RATSNEST;');stage('orphans-on-visible');time.sleep(5)
stage('validation');panel.update(status='Check the native result',connection='Connectivity and DRC after native refill',plan='0.1 mm is a process limit, not a high-voltage safety rule',checks='Creepage and clearance depend on voltage and environment',event='Native verification',percent=95)
body="s=handle(app,{'expectDocument':target.name},'routing_state')\nresult=handle(app,{'expectDocument':target.name,'boardId':s['boardId'],'expectedRevision':s['revision']},'routing_drc')"
result=native(body);(TAKE/'final.json').write_text(json.dumps(result,indent=2));assert result['success'] and result['drc']['airwireCount']==0 and result['drc']['errorCount']==1 and result['drc']['warningCount']==0,result.get('drc')
r=native("root,b,m=snapshot(app,{'expectDocument':target.name})\nout=Path('C:/Users/john/AppData/Local/Temp/adom-ablation/bq25792-ablation-routed.brd')\nET.ElementTree(root).write(out,encoding='utf-8',xml_declaration=True)\napp.executeTextCommand('Document.CopyToDesktop C:/Users/john/AppData/Local/Temp/adom-ablation/bq25792-ablation-routed.fbrd')\nresult={'path':out.as_posix()}")
invoke('pull_file',{'filePaths':[r['path'],'C:/Users/john/AppData/Local/Temp/adom-ablation/bq25792-ablation-routed.fbrd'],'saveTo':str(TAKE)})
from coverage import measure
coverage=measure(TAKE/'bq25792-ablation-routed.brd');(TAKE/'coverage.json').write_text(json.dumps(coverage,indent=2))
panel.update(status='Copper retained on all layers',connection='50 nets connected / 612 segments / 60 vias / 4 pours',plan=f"{coverage['combinedCoveragePercent']:.1f}% copper coverage / {coverage['combinedRemovalReductionPercent']:.1f}% less projected removal area",checks='0 airwires / 0 warnings / 0 new DRC errors\n1 unchanged placement error; electrical review remains',event='Measured from native filled export',percent=100)
command('DISPLAY NONE; DISPLAY 1 17 18 20 21; WINDOW (-1mm -1mm) (52mm 28mm);');stage('final-visible');time.sleep(6)
finally:
paths=[]
for name,r in records:
stop=call('desktop_record_window_stop',{'recordingId':r['recordingId']});(TAKE/(name+'-stop.json')).write_text(json.dumps(stop,indent=2));assert stop.get('success'),stop;paths.append(stop['path'])
if paths:invoke('pull_file',{'filePaths':paths,'saveTo':str(TAKE)})
print('Recorded, exported, natively validated',flush=True)