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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
import sys,subprocess,json
from pathlib import Path
import shutil
F=shutil.which('ffmpeg');root=Path(__file__).parent
HEADER='''[Script Info]
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
WrapStyle: 2
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Main,Liberation Sans,34,&H00FFFFFF,&H00FFFFFF,&H00130F0A,&H00130F0A,0,0,0,0,100,100,0,0,1,1,0,2,30,30,22,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
'''
def stamp(t):
cs=round(t*100);return f'{cs//360000}:{cs//6000%60:02d}:{cs//100%60:02d}.{cs%100:02d}'
def render(name,source,start,end,cues,side,title,hold=4,keep=None,action_seconds=None,frame_ranges=None,capture_note="Idle waits trimmed"):
duration=(action_seconds if action_seconds is not None else (sum(b-a for a,b in keep) if keep else end-start)/3)+hold
events=[]
def text(t0,t1,s):events.append(f'Dialogue: 0,{stamp(t0)},{stamp(t1)},Main,,0,0,0,,{s}')
text(0,duration,r'{\an7\pos(34,16)\fs38\b1\c&H00E5CF65&}CODEX ASTRA IN ADOM HYDROGEN'+r'{\c&H00FFFFFF&} / '+title)
text(0,duration,r'{\an7\pos(1490,125)\fs48\b1\c&H00E5CF65&}3x SPEED')
text(0,duration,r'{\an7\pos(1490,197)\fs28}Native Fusion capture\N'+capture_note)
for y,heading,body in side:
text(0,duration,fr'{{\an7\pos(1490,{y})\fs32\b1\c&H00E5CF65&}}'+heading)
text(0,duration,fr'{{\an7\pos(1490,{y+46})\fs27}}'+body)
for a,b,s in cues:text(a,b,r'{\an2\pos(960,1058)\fs34}'+s)
text(duration-hold-.2,duration,r'{\an7\pos(1490,885)\fs23\c&H00B9B9B9&}Final frame held for reading')
ass=root/(name+'.ass');ass.write_text(HEADER+'\n'.join(events)+'\n')
out=root/(name+'.mp4')
selection=("select='"+'+'.join(f'between(t,{a},{b})' for a,b in keep)+"',setpts=N/(30*TB)" if keep else f'trim=start={start}:end={end}')
if frame_ranges is not None:
selection="select='"+'+'.join(f'gte(n,{a})*lt(n,{b})' for a,b in frame_ranges)+"',setpts=N/(30*TB)"
vf=f'{selection},setpts=(PTS-STARTPTS)/3,fps=30,scale=1440:900:force_original_aspect_ratio=decrease,pad=1440:900:(ow-iw)/2:(oh-ih)/2:color=0x0a0f13,pad=1920:1080:0:70:color=0x0a0f13,tpad=stop_mode=clone:stop_duration={hold},ass={ass}'
subprocess.run([F,'-hide_banner','-loglevel','warning','-i',str(source),'-vf',vf,'-an','-c:v','libx264','-preset','fast','-crf','20','-pix_fmt','yuv420p','-movflags','+faststart','-y',str(out)],check=True)
(root/(name+'.edit.json')).write_text(json.dumps({'source':str(source),'trimStartSeconds':start,'trimEndSeconds':end,'speed':3,'holdSeconds':hold,'durationSeconds':duration,'captions':cues,'keptSourceRanges':keep,'keptFrameRangesEndExclusive':frame_ranges},indent=2))
print(out,flush=True)
if __name__=='__main__':
import argparse
p=argparse.ArgumentParser();p.add_argument('take',type=Path);a=p.parse_args()
source=next(a.take.glob('window-*.mp4'))
# select + N/(30*TB) removes gaps correctly only for verified constant-30fps input.
frames=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-show_entries','frame=best_effort_timestamp_time','-of','json',str(source)]))['frames']
pts=[float(f['best_effort_timestamp_time']) for f in frames]
assert all(abs((b-a)-1/30)<.00001 for a,b in zip(pts,pts[1:])), 'VFR source needs trim/concat'
keep=json.loads((a.take/'action-ranges.json').read_text())
final=json.loads((a.take/'final.json').read_text())
assert final['segmentCount']==120 and final['viaCount']==48 and len(final['pads'])==72 and not final['remainingNets'] and final['drc']['clean']
selected=[i for i,t in enumerate(pts) if any(lo<=t<=hi for lo,hi in keep)]
frame_ranges=[]
for i in selected:
if frame_ranges and i==frame_ranges[-1][1]:frame_ranges[-1][1]=i+1
else:frame_ranges.append([i,i+1])
retained=len(selected)
action=retained/90;duration=action+5
cues=[
(0,4,'Codex Astra calculates the 24-net crossover in Adom Hydrogen.'),
(4,8,'Output column j = (7 x i) mod 24. Rows and columns use 2.54 mm pitch.'),
(8,12,r'Top copper carries rows; bottom copper carries columns.\NCrossings stay on separate layers. No Freerouting.'),
(12,16,r'fusion_routing_state reads nets, pads and revision.\Nfusion_route_net dryRun checks explicit planned geometry.'),
(16,20,r'fusion_route_net adds each trace and via through Adom Bridge.\NexpectedRevision guards edits; copper is read back after each call.'),
(20,action,r'Each net branches to a third pad and uses two through vias.\NFusion recalculates connectivity as the copper appears.'),
(action,duration,r'fusion_routing_drc: 24 nets connected.\NNative Autodesk DRC: 0 errors / 0 warnings / 0 airwires.')]
# Keep captions legible if the actual action is shorter than the template.
if action<23:
factor=action/24
cues=[(x*factor,y*factor,t) for x,y,t in cues[:-2]]+[(20*factor,action,cues[-2][2]),(action,duration,cues[-1][2])]
render('fusion-crossbar-24-3x',source,0,pts[-1]+1/30,cues,
[(305,'THE BOARD',r'24 nets / 72 pads\N120 trace segments\N48 through vias'),
(495,'THE CALCULATION',r'j = (7 x i) mod 24\N0.30 mm trace width\NExplicit Astra waypoints'),
(685,'RECORD + VERIFY',r'Native AB window capture\NFusion copper read-back\NAutodesk DRC result')],
'24-NET CROSSOVER',hold=5,keep=keep,action_seconds=action,frame_ranges=frame_ranges)