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.
← Commit history
Publish 1.9.255
3 files changed
+32−22
.release-page-head+1−1@@ -1 +1 @@-a5785bfd093f39b6bc312e1d10b6ddd4993ba465+3eba99ebadcfc0d353c44e7dccc8da0917b4e40f
dashboard/ui/quick-actions.html+24−11@@ -67,7 +67,14 @@ input[type=text]{font:inherit;color:var(--text);background:var(--surface);border .live-head{display:flex;align-items:center;gap:8px;font:11px var(--mono);color:var(--text-2);margin-bottom:6px;letter-spacing:.04em;text-transform:uppercase} .live-led{width:8px;height:8px;border-radius:50%;background:var(--text-2)}.live-led.on{background:var(--red);box-shadow:0 0 6px var(--red);animation:blink 1s steps(2) infinite} @keyframes blink{50%{opacity:.35}}+.live img[hidden]{display:none} .live img{display:block;width:100%;height:auto;border-radius:6px;min-height:120px;background:#111}+.toggle{background:none;border:none;padding:2px;cursor:pointer;flex-shrink:0;transition:transform .06s ease}+.toggle-track{display:block;width:32px;height:18px;background:var(--overlay);border:1px solid var(--border);border-radius:9px;position:relative;transition:background .15s ease,border-color .15s ease}+.toggle:hover .toggle-track{border-color:var(--accent)}.toggle:active{transform:translateY(1px) scale(.97)}+.toggle.active .toggle-track{background:var(--accent);border-color:var(--accent)}+.toggle-thumb{position:absolute;top:2px;left:2px;width:12px;height:12px;background:#fff;border-radius:50%;transition:transform .18s ease}+.toggle.active .toggle-thumb{transform:translateX(14px)} .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px} .chip{font:12px var(--mono);color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:999px;padding:5px 11px;cursor:pointer;min-height:32px;transition:border-color .15s ease,transform .06s ease} .chip:hover{border-color:var(--accent)}.chip:active{transform:translateY(1px)}.chip.active{border-color:var(--accent);color:var(--accent)}@@ -135,8 +142,9 @@ input[type=text]{font:inherit;color:var(--text);background:var(--surface);border <section class="card" data-card="part"> <div class="card-title">Part to import <span class="card-status" id="partStatus"></span></div> <div class="card-desc">A wiki component page publishes its Fusion files (<code>.lbr</code> plus a <code>.step</code>). Resolve them here; the import below zips them in the browser and stages the zip on this machine in one call.</div>- <div class="row"><input type="text" id="partRef" value="ray/dac80504" aria-label="Wiki page reference" placeholder="owner/slug"><button class="btn" id="btnResolve" data-action="resolve">Resolve files</button></div>- <div class="row" style="align-items:baseline"><span class="mini">or pick one of Ray's recent parts:</span><button class="btn" id="btnShuffle" data-action="shuffle" style="min-height:32px;padding:4px 10px">Shuffle</button></div>+ <div class="row"><input type="text" id="partRef" value="ray/dac80504" aria-label="Wiki page reference" placeholder="owner/slug, or several separated by commas for one multi-part library"><button class="btn" id="btnResolve" data-action="resolve">Resolve files</button></div>+ <div class="row" style="align-items:center"><label class="mini" style="display:flex;align-items:center;gap:8px;cursor:pointer"><button class="toggle" id="tglNoModel" role="switch" aria-checked="false" aria-label="Skip the 3D model" data-action="skip_model"><span class="toggle-track"><span class="toggle-thumb"></span></span></button>skip the 3D model (symbol + footprint only; the library installs unbound)</label></div>+ <div class="row" style="align-items:baseline"><span class="mini">or pick one of Ray's recent parts (click several to build a multi-part library):</span><button class="btn" id="btnShuffle" data-action="shuffle" style="min-height:32px;padding:4px 10px">Shuffle</button></div> <div class="chips" id="chips"></div> <div class="kv" id="partKv"></div> </section>@@ -223,25 +231,30 @@ function cmd(command,args,opts){opts=opts||{};if(!bridge){return Promise.reject( function connect(){status($('connStatus'),'probing 127.0.0.1:24680-24689...');$('connNote').textContent='allow local network access if your browser asks';$('led').className='led';return probe().then(function(b){bridge=b;if(!b){status($('connStatus'),'Adom Bridge not detected','bad');$('led').className='led bad';$('conn').textContent='not connected';$('meta').textContent='is Adom Bridge running?';toast('Adom Bridge not found on 24680-24689. Is ab running on this machine?','err');return null}var wc=b.raw.webControl||{};var m=b.raw.machine;var name=(m&&typeof m==='object')?(m.clientName||m.hostname||'this machine'):(m||'this machine');$('led').className='led '+(wc.enabled?'on':'warn');$('conn').textContent=name+' :'+b.port;$('meta').textContent='ab '+(b.raw.version||'?')+' · web control '+(wc.enabled?'on':'OFF')+(wc.grantedForYou?' · this page is granted':(wc.promptExpected?' · ab will ask you per command':''));$('bpAb').textContent=(b.raw.version||'?')+' on '+name;status($('connStatus'),wc.enabled?'connected, web control enabled':'connected, web control is OFF in ab Settings',wc.enabled?'ok':'warn');$('connNote').textContent='';kv($('connKv'),{machine:name,'ab version':b.raw.version||'?',endpoint:'http://127.0.0.1:'+b.port,'web control':wc.enabled?'enabled':'disabled','this page':wc.grantedForYou?'granted (no prompts)':(wc.promptExpected?'ab asks you once per command (Allow once / Allow 1 hr / Deny)':'unknown'),'handshake required':wc.requireHandshake?'yes':'no'});toast('Connected to Adom Bridge on '+name,'ok');$('btnConnect').textContent='Reconnect';return b})} // ---- part files -----------------------------------------------------------------------function resolvePart(rec){var ref=$('partRef').value.trim().replace(/^\/+|\/+$/g,'');if(!ref)return;rec=rec&&rec.slug?rec:(catalogPages[ref]||null);status($('partStatus'),'resolving...');part=null;$('btnImport').disabled=true;return fetch('https://wiki.adom.inc/api/v1/pages/'+ref+'/files',{signal:timeout(15000)}).then(function(r){if(!r.ok)throw new Error('wiki answered '+r.status);return r.json()}).then(function(j){var files=j.files||[];var lbr=files.filter(function(f){return /\.lbr$/i.test(f.name)});var steps=files.filter(function(f){return /\.(step|stp)$/i.test(f.name)});if(!lbr.length)throw new Error('this page publishes no .lbr');var base='https://wiki.adom.inc/api/v1/pages/'+ref+'/files/';part={ref:ref,name:ref.split('/').pop(),lbr:{name:lbr[0].name,url:base+lbr[0].name,size:lbr[0].size},steps:steps.map(function(f){return{name:f.name,url:base+f.name,size:f.size}})};var lc=(j.files||[]).map(function(f){return (f.lastCommit||{}).timestamp||0}).reduce(function(a,b){return Math.max(a,b)},0);var info={page:'https://wiki.adom.inc/'+ref};if(rec){info.title=(rec.title||'')+(rec.brief?' - '+String(rec.brief).slice(0,90):'');info.version=rec.version||'';info.published=fmtDate(rec.last_commit_at||rec.updated_at)+(rec.created_at?' (page created '+fmtDate(rec.created_at)+')':'')}else if(lc){info['files published']=fmtDate(lc*1000)}info.library=part.lbr.name+' ('+part.lbr.size+' B)';info.models=part.steps.map(function(s){return s.name+' ('+s.size+' B)'}).join(', ')||'none (the import will be an unbound library)';kv($('partKv'),info);status($('partStatus'),'ready','ok');$('btnImport').disabled=false;$('importNote').textContent=part.steps.length?'one call, then the page polls':'no STEP on this page: nothing to bind';toast('Resolved '+part.lbr.name+(part.steps.length?' + '+part.steps.length+' model(s)':''),'ok')}).catch(function(e){status($('partStatus'),e.message,'bad');toast('Could not resolve the part: '+e.message,'err')})}-+var skipModel=false;+function fetchPartFiles(ref){return fetch('https://wiki.adom.inc/api/v1/pages/'+ref+'/files',{signal:timeout(15000)}).then(function(r){if(!r.ok)throw new Error(ref+': wiki answered '+r.status);return r.json()}).then(function(j){var files=j.files||[];var lbr=files.filter(function(f){return /\.lbr$/i.test(f.name)});var steps=files.filter(function(f){return /\.(step|stp)$/i.test(f.name)});if(!lbr.length)throw new Error(ref+' publishes no .lbr');var base='https://wiki.adom.inc/api/v1/pages/'+ref+'/files/';return{ref:ref,name:ref.split('/').pop(),lbr:{name:lbr[0].name,url:base+lbr[0].name,size:lbr[0].size},steps:steps.map(function(f){return{name:f.name,url:base+f.name,size:f.size}}),lastCommit:files.map(function(f){return (f.lastCommit||{}).timestamp||0}).reduce(function(a,b){return Math.max(a,b)},0)}})}+function resolvePart(rec){var refs=$('partRef').value.split(',').map(function(x){return x.trim().replace(/^\/+|\/+$/g,'')}).filter(Boolean);if(!refs.length)return;status($('partStatus'),'resolving '+refs.length+' page'+(refs.length===1?'':'s')+'...');part=null;$('btnImport').disabled=true;return Promise.all(refs.map(fetchPartFiles)).then(function(ps){var multi=ps.length>1;part={refs:refs,parts:ps,multi:multi,name:multi?('adom-multi-'+ps.length+'parts'):ps[0].name,lbr:ps[0].lbr,steps:skipModel?[]:[].concat.apply([],ps.map(function(p){return p.steps}))};var info={};if(multi){info.library=part.name+' (merged from '+ps.length+' pages)';ps.forEach(function(p,i){var r=catalogPages[p.ref];info['part '+(i+1)]=p.ref+(r?' · published '+fmtDate(r.last_commit_at||r.updated_at):'')+' · '+p.lbr.name+(p.steps.length?' + '+p.steps[0].name:' (no model)')})}else{var p=ps[0];rec=rec&&rec.slug?rec:(catalogPages[p.ref]||null);info.page='https://wiki.adom.inc/'+p.ref;if(rec){info.title=(rec.title||'')+(rec.brief?' - '+String(rec.brief).slice(0,90):'');info.version=rec.version||'';info.published=fmtDate(rec.last_commit_at||rec.updated_at)+(rec.created_at?' (page created '+fmtDate(rec.created_at)+')':'')}else if(p.lastCommit){info['files published']=fmtDate(p.lastCommit*1000)}info.library=p.lbr.name+' ('+p.lbr.size+' B)';info.models=p.steps.map(function(s){return s.name+' ('+s.size+' B)'}).join(', ')||'none'}if(skipModel)info.models='SKIPPED by the switch above: the library will install unbound';kv($('partKv'),info);status($('partStatus'),multi?ps.length+' parts, ready':'ready','ok');$('btnImport').disabled=false;$('importNote').textContent=part.steps.length?(multi?'one call for all '+ps.length+' parts, then the page polls':'one call, then the page polls'):'no STEP: nothing to bind, the library installs unbound';toast('Resolved '+(multi?ps.length+' parts':ps[0].lbr.name)+(part.steps.length?' + '+part.steps.length+' model(s)':' (no models)'),'ok')}).catch(function(e){status($('partStatus'),e.message,'bad');toast('Could not resolve: '+e.message,'err')})}+// Merge N EAGLE libraries into one multi-deviceset library (packages, symbols, devicesets appended; first library's header kept).+function mergeLbrs(texts){if(texts.length===1)return texts[0];var P=new DOMParser();var base=P.parseFromString(texts[0],'application/xml');function sec(doc,tag){var el=doc.querySelector('library > '+tag);if(!el){el=doc.createElement(tag);doc.querySelector('library').appendChild(el)}return el}var seen={packages:{},symbols:{},devicesets:{}};['packages','symbols','devicesets'].forEach(function(t){[].forEach.call(sec(base,t).children,function(c){seen[t][c.getAttribute('name')]=1})});for(var i=1;i<texts.length;i++){var d=P.parseFromString(texts[i],'application/xml');['packages','symbols','devicesets'].forEach(function(t){var src=d.querySelector('library > '+t);if(!src)return;[].forEach.call(src.children,function(c){var n=c.getAttribute('name');if(seen[t][n])return;seen[t][n]=1;sec(base,t).appendChild(base.importNode(c,true))})});var p3=d.querySelector('library > packages3d');if(p3){var b3=sec(base,'packages3d');[].forEach.call(p3.children,function(c){b3.appendChild(base.importNode(c,true))})}}return new XMLSerializer().serializeToString(base)} // ---- Ray's recent parts: random picks with their publish date -------------------------- var catalogPages={},rayPages=null; function fmtDate(v){if(!v)return '';var d=new Date(typeof v==='number'?v:String(v).replace(' ','T'));if(isNaN(d))return String(v);return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})} function loadRay(){if(rayPages)return Promise.resolve(rayPages);return fetch('https://wiki.adom.inc/api/v1/pages?owner=ray&limit=500',{signal:timeout(15000)}).then(function(r){return r.json()}).then(function(j){rayPages=(j.pages||[]).filter(function(p){return p.kind==='component'});rayPages.forEach(function(p){catalogPages[p.owner+'/'+p.slug]=p});rayPages.sort(function(a,b){return String(b.last_commit_at||'').localeCompare(String(a.last_commit_at||''))});return rayPages})}-function shuffle(){$('chips').innerHTML='<span class="mini">loading Ray\'s parts...</span>';loadRay().then(function(ps){var recent=ps.slice(0,120),pick=[];while(pick.length<6&&recent.length){var i=Math.floor(Math.random()*recent.length);pick.push(recent.splice(i,1)[0])}$('chips').innerHTML='';pick.forEach(function(p){var b=document.createElement('button');b.className='chip';b.setAttribute('data-action','pick');b.setAttribute('data-ref',p.owner+'/'+p.slug);b.innerHTML=esc(p.slug)+'<small>'+esc(fmtDate(p.last_commit_at||p.updated_at).replace(/,? \d{4}/,''))+'</small>';b.title=(p.title||'')+' - '+(p.brief||'');b.onclick=function(){[].forEach.call(document.querySelectorAll('.chip'),function(c){c.className='chip'});b.className='chip active';$('partRef').value=p.owner+'/'+p.slug;resolvePart(p)};$('chips').appendChild(b)});var newest=ps[0];if(newest)$('chips').insertAdjacentHTML('beforeend','<span class="mini" style="align-self:center">'+ps.length+' parts by Ray, newest '+esc(fmtDate(newest.last_commit_at))+'</span>')}).catch(function(e){$('chips').innerHTML='<span class="mini">could not list Ray\'s pages: '+esc(e.message)+'</span>'})}+function shuffle(){$('chips').innerHTML='<span class="mini">loading Ray\'s parts...</span>';loadRay().then(function(ps){var recent=ps.slice(0,120),pick=[];while(pick.length<6&&recent.length){var i=Math.floor(Math.random()*recent.length);pick.push(recent.splice(i,1)[0])}$('chips').innerHTML='';pick.forEach(function(p){var b=document.createElement('button');b.className='chip';b.setAttribute('data-action','pick');b.setAttribute('data-ref',p.owner+'/'+p.slug);b.innerHTML=esc(p.slug)+'<small>'+esc(fmtDate(p.last_commit_at||p.updated_at).replace(/,? \d{4}/,''))+'</small>';b.title=(p.title||'')+' - '+(p.brief||'');b.onclick=function(){var ref=p.owner+'/'+p.slug;var cur=$('partRef').value.split(',').map(function(x){return x.trim()}).filter(Boolean);if(b.className.indexOf('active')>=0){cur=cur.filter(function(x){return x!==ref});b.className='chip'}else{if(!$('partRef').dataset.multiStart)cur=[];cur.push(ref);b.className='chip active'}$('partRef').dataset.multiStart='1';$('partRef').value=cur.join(', ');resolvePart(cur.length===1?p:null)};$('chips').appendChild(b)});var newest=ps[0];if(newest)$('chips').insertAdjacentHTML('beforeend','<span class="mini" style="align-self:center">'+ps.length+' parts by Ray, newest '+esc(fmtDate(newest.last_commit_at))+'</span>')}).catch(function(e){$('chips').innerHTML='<span class="mini">could not list Ray\'s pages: '+esc(e.message)+'</span>'})} // ---- zip (STORE only) built in the browser ------------------------------------------ var CRC=(function(){var t=[];for(var n=0;n<256;n++){var c=n;for(var k=0;k<8;k++)c=c&1?0xEDB88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); function crc32(u8){var c=0xFFFFFFFF;for(var i=0;i<u8.length;i++)c=CRC[(c^u8[i])&0xFF]^(c>>>8);return (c^0xFFFFFFFF)>>>0} function makeZip(entries){var parts=[],central=[],offset=0,enc=new TextEncoder();entries.forEach(function(en){var name=enc.encode(en.name),data=new Uint8Array(en.data),crc=crc32(data);var lh=new DataView(new ArrayBuffer(30));lh.setUint32(0,0x04034b50,true);lh.setUint16(4,20,true);lh.setUint16(6,0,true);lh.setUint16(8,0,true);lh.setUint16(10,0,true);lh.setUint16(12,0x21,true);lh.setUint32(14,crc,true);lh.setUint32(18,data.length,true);lh.setUint32(22,data.length,true);lh.setUint16(26,name.length,true);lh.setUint16(28,0,true);parts.push(new Uint8Array(lh.buffer),name,data);var ch=new DataView(new ArrayBuffer(46));ch.setUint32(0,0x02014b50,true);ch.setUint16(4,20,true);ch.setUint16(6,20,true);ch.setUint16(8,0,true);ch.setUint16(10,0,true);ch.setUint16(12,0,true);ch.setUint16(14,0x21,true);ch.setUint32(16,crc,true);ch.setUint32(20,data.length,true);ch.setUint32(24,data.length,true);ch.setUint16(28,name.length,true);ch.setUint16(30,0,true);ch.setUint16(32,0,true);ch.setUint16(34,0,true);ch.setUint16(36,0,true);ch.setUint32(38,0,true);ch.setUint32(42,offset,true);central.push(new Uint8Array(ch.buffer),name);offset+=30+name.length+data.length});var cdSize=central.reduce(function(a,b){return a+b.length},0);var eocd=new DataView(new ArrayBuffer(22));eocd.setUint32(0,0x06054b50,true);eocd.setUint16(8,entries.length,true);eocd.setUint16(10,entries.length,true);eocd.setUint32(12,cdSize,true);eocd.setUint32(16,offset,true);var all=parts.concat(central,[new Uint8Array(eocd.buffer)]);var total=all.reduce(function(a,b){return a+b.length},0),out=new Uint8Array(total),pos=0;all.forEach(function(b){out.set(b,pos);pos+=b.length});return out.buffer}-function stage(path,buf,note){var u=new Uint8Array(buf),CH=1024*1024,n=Math.max(1,Math.ceil(u.length/CH));function one(off){var end=Math.min(off+CH,u.length),fin=end>=u.length;setStage(note+' (chunk '+(Math.floor(off/CH)+1)+'/'+n+')');return cmd('write_file_chunk',{path:path,dataB64:toB64(u.subarray(off,end)),offset:off,final:fin,createDirs:true}).then(function(d){if(fin)return d.path||path;return one(typeof d.nextOffset==='number'?d.nextOffset:end)})}return one(0)}+function granted(){return !!(bridge&&bridge.raw&&bridge.raw.webControl&&bridge.raw.webControl.grantedForYou)}+function askNote(){return granted()?'':' · Adom Bridge is asking you on this machine: click Allow (1 hr) in its toast'}+function stage(path,buf,note){var u=new Uint8Array(buf),CH=1024*1024,n=Math.max(1,Math.ceil(u.length/CH));function one(off){var end=Math.min(off+CH,u.length),fin=end>=u.length;setStage(note+' (chunk '+(Math.floor(off/CH)+1)+'/'+n+')'+askNote());return cmd('write_file_chunk',{path:path,dataB64:toB64(u.subarray(off,end)),offset:off,final:fin,createDirs:true}).then(function(d){if(fin)return d.path||path;return one(typeof d.nextOffset==='number'?d.nextOffset:end)})}return one(0)} // ---- import: one call, then poll -------------------------------------------------------- function setProg(p,cls){var el=$('prog');el.className='prog '+(cls||'');el.firstElementChild.style.width=Math.max(0,Math.min(100,p||0))+'%'} function setStage(html){$('stage').innerHTML=html} function renderLint(pm){if(!pm){$('lint').innerHTML='';return}var ok=pm.ok!==false;var mc=pm.missingCounts||{};var tot=pm.componentsTotal||pm.variantsTotal||0;var parts=[];['MPN','MANUFACTURER'].forEach(function(k){if(mc[k])parts.push(k.toLowerCase()==='mpn'?'manufacturer part number':'manufacturer')});if(pm.componentsWithoutAnyDistributor)parts.push('a distributor number (Mouser, DigiKey or LCSC)');var n=Math.max(pm.componentsWithoutAnyDistributor||0,mc.MPN||0,mc.MANUFACTURER||0,mc.MOUSER_PN||0,mc.DIGIKEY_PN||0,mc.LCSC_PN||0)||tot;var line=ok?'every component carries a manufacturer part number and a distributor number.':(tot?n+' of '+tot+' component'+(tot===1?'':'s')+' missing ':'missing ')+(parts.join(', ')||'part numbers')+'. The import is fine; the BOM will be blank for it until the numbers are added upstream.';$('lint').innerHTML='<div class="lint '+(ok?'ok':'warn')+'"><b>Part numbers</b> '+esc(line)+'</div>'}-function renderReceipt(d){var unb=(d.unbound||[]).map(function(u){return u.package+' ('+u.why+')'}).join('; ');kv($('receipt'),{installed:d.installed?'yes, '+d.libraryPath:'no',library:d.libraryName||'',parts:(d.partsToBind||0)+' of '+(d.partsTotal||0)+' paired with a STEP','named skips':unb||'none','fusion':(d.fusion&&d.fusion.message)||'',runId:d.runId||'','estimated':d.estimatedSec?Math.round(d.estimatedSec)+'s':''});renderLint(d.partMetadata)}+function renderReceipt(d){var unb=(d.unbound||[]).map(function(u){return u.package+' ('+u.why+')'}).join('; ');kv($('receipt'),{installed:d.installed?'yes, '+d.libraryPath:'no',library:d.libraryName||'',parts:(d.partsToBind||0)+' of '+(d.partsTotal||0)+' paired with a STEP'+((d.parts||[]).length>1?': '+d.parts.map(function(p){return p.package}).join(', '):''),'named skips':unb||'none','fusion':(d.fusion&&d.fusion.message)||'',runId:d.runId||'','estimated':d.estimatedSec?Math.round(d.estimatedSec)+'s':''});renderLint(d.partMetadata)} var frameCache={}; function frameSrc(f){var src=f.thumbnailDataUrl||f.dataUrl;if(src)return Promise.resolve(src);var p=f.screenshot||f.path;if(!p)return Promise.resolve(null);if(frameCache[p])return Promise.resolve(frameCache[p]);return cmd('read_file',{path:p,encoding:'base64'},{timeoutMs:60000}).then(function(d){var c=d.content||'';if(!c)return null;var u='data:image/'+(/\.png$/i.test(p)?'png':'webp')+';base64,'+c;frameCache[p]=u;return u}).catch(function(){return null})} function renderFrames(ev){var g=$('gallery');g.innerHTML='';(ev||[]).forEach(function(f,i){var id='shot'+i;var h='<div class="shot" id="'+id+'"><div class="cap" style="padding:18px 8px">'+(f.ok===false?'REFUSED: '+esc(f.error||'not evidence'):'loading frame...')+'</div><div class="cap"><span><span class="tag '+(f.kind==='proof'?'proof':'feed')+'">'+esc(f.kind||'frame')+'</span> '+esc(f.step||f.view||'')+(f.width?' '+f.width+'x'+f.height:'')+'</span><span>'+esc(f.part||'')+'</span></div></div>';g.insertAdjacentHTML('beforeend',h);if(f.ok===false)return;frameSrc(f).then(function(src){var el=document.getElementById(id);if(!el)return;if(src){el.firstElementChild.outerHTML='<img src="'+src+'" alt="'+esc(f.step||f.view||'frame')+'">'}else{el.firstElementChild.textContent='frame could not be fetched from the box: '+((f.screenshot||'').split('/').pop())}})})}@@ -250,13 +263,13 @@ var live={timer:null,since:0,on:false}; function liveStart(){if(live.on)return;live.on=true;live.since=0;$('live').hidden=false;$('liveImg').hidden=true;$('liveImg').removeAttribute('src');$('liveLabel').textContent='live · Fusion on this machine';$('liveLed').className='live-led';$('liveTick').textContent='';$('liveNote').textContent='starting the 1 fps capture on this machine...';cmd('fusion_live_feed',{action:'start',intervalSec:1,maxSec:1500}).then(function(){liveTick()}).catch(function(e){$('liveNote').textContent='feed could not start: '+e.message})} function liveTick(){if(!live.on)return;cmd('fusion_live_feed',{action:'frames',since:live.since},{timeoutMs:20000}).then(function(d){var fr=(d.frames||[]).filter(function(f){return f.kind==='main'});if(d.healthy===false&&!fr.length){$('liveLed').className='live-led';$('liveNote').textContent=(d.noHwnd&&!d.captured)?'waiting for the Fusion window (Fusion is launching)':(d._honesty||'feed is not capturing');}if(fr.length){var f=fr[fr.length-1];live.since=Math.max(live.since,f.tick);return frameSrc(f).then(function(src){if(src&&live.on){$('liveImg').src=src;$('liveImg').hidden=false;$('liveLed').className='live-led on';$('liveTick').textContent='frame '+f.tick+' · '+(f.popups?f.popups+' dialog(s) over it':'no dialogs');$('liveNote').textContent=f.title||''}})}}).catch(function(e){$('liveNote').textContent='feed: '+e.message}).then(function(){if(live.on)live.timer=setTimeout(liveTick,1500)})} function liveStop(why){if(!live.on)return;live.on=false;clearTimeout(live.timer);$('liveLed').className='live-led';$('liveLabel').textContent='live view ended · '+(why||'');cmd('fusion_live_feed',{action:'stop'}).catch(function(){})}-function poll(id){runId=id;clearTimeout(pollTimer);var tick=0;liveStart();(function loop(){tick++;cmd('fusion_prove_result',{runId:id}).then(function(d){var r=d.result||d;var pct=typeof r.percent==='number'?r.percent:null;if(r.stage==='failed'){liveStop('failed');setProg(100,'fail');status($('importStatus'),r.errorCode==='bridge_restarted_mid_run'?'bridge restarted mid-run':'failed','bad');renderFrames(r.evidence);if(r.errorCode==='bridge_restarted_mid_run'){showRestartedMidRun(r);return}setStage('<b>failed</b> '+esc(r.error||(r.bind&&r.bind.error)||r.errorCode||''));toast('Import failed: '+(r.error||(r.bind&&r.bind.error)||'see the log'),'err');return}if(r.stage==='done'){liveStop('done');setProg(100,'done');var pv=r.prove||{};setStage('<b>done</b> verdict: '+esc(r.verdict||(pv.proven?'proven':'not proven'))+' · '+(pv.partsProven||0)+'/'+(pv.partsTotal||r.partsTotal||0)+' parts proven · '+(r.evidenceCount||0)+' frames');status($('importStatus'),r.verdict==='proven'||pv.proven?'proven':'finished, not proven',r.verdict==='proven'||pv.proven?'ok':'warn');if(r.partMetadata)renderLint(r.partMetadata);toast('Library created in Fusion: '+(pv.partsProven||0)+'/'+(pv.partsTotal||0)+' parts proven','ok');return cmd('fusion_prove_result',{runId:id,inlineFrames:true},{timeoutMs:120000}).then(function(d2){var r2=d2.result||d2;renderFrames(r2.evidence||r.evidence)}).catch(function(){renderFrames(r.evidence)})}if(pct!==null)setProg(pct);setStage('<b>'+esc(r.stage||'working')+'</b>'+(r.stageLabel?' · '+esc(r.stageLabel):'')+(pct!==null?' · '+Math.round(pct)+'%':'')+(typeof r.etaSec==='number'?' · ~'+Math.round(r.etaSec)+'s left':''));if(r.evidence&&r.evidence.length)renderFrames(r.evidence);if(r.errorCode==='addin_restart_required')showAsk(r);pollTimer=setTimeout(loop,tick<3?5000:10000)}).catch(function(e){var notYet=e.code==='run_not_found'||e.code==='unknown_run_id'||/no prove run/i.test(e.message||'');var transient=notYet||/error sending request|unreachable|HTTP 0|HTTP 5\d\d|timed out|main_thread_busy/i.test(e.message||'');if(transient&&tick<12){setStage('<b>'+(notYet?'starting (the run is not on file yet)':'bridge not answering, retrying')+'</b> · '+esc((e.message||'').slice(0,90))+' (try '+tick+' of 12)');pollTimer=setTimeout(loop,8000);return}liveStop('poll failed');setProg(100,'fail');setStage('<b>poll failed</b> '+esc(e.message));status($('importStatus'),'poll failed','bad')})})()}+function poll(id){runId=id;clearTimeout(pollTimer);var tick=0;liveStart();(function loop(){tick++;cmd('fusion_prove_result',{runId:id}).then(function(d){var r=d.result||d;var pct=typeof r.percent==='number'?r.percent:null;if(r.stage==='failed'){liveStop('failed');setProg(100,'fail');status($('importStatus'),r.errorCode==='bridge_restarted_mid_run'?'bridge restarted mid-run':'failed','bad');renderFrames(r.evidence);if(r.errorCode==='bridge_restarted_mid_run'){showRestartedMidRun(r);return}setStage('<b>failed</b> '+esc(r.error||(r.bind&&r.bind.error)||r.errorCode||''));toast('Import failed: '+(r.error||(r.bind&&r.bind.error)||'see the log'),'err');return}if(r.stage==='done'){liveStop('done');setProg(100,'done');var pv=r.prove||{};setStage('<b>done</b> verdict: '+esc(r.verdict||(pv.proven?'proven':'not proven'))+' · '+(pv.partsProven||0)+'/'+(pv.partsTotal||r.partsTotal||0)+' parts proven · '+(r.evidenceCount||0)+' frames');status($('importStatus'),r.verdict==='proven'||pv.proven?'proven':'finished, not proven',r.verdict==='proven'||pv.proven?'ok':'warn');if(r.partMetadata)renderLint(r.partMetadata);toast('Library created in Fusion: '+(pv.partsProven||0)+'/'+(pv.partsTotal||0)+' parts proven','ok');return cmd('fusion_prove_result',{runId:id,inlineFrames:true},{timeoutMs:120000}).then(function(d2){var r2=d2.result||d2;renderFrames(r2.evidence||r.evidence)}).catch(function(){renderFrames(r.evidence)})}if(pct!==null)setProg(pct);setStage('<b>'+esc(r.stage||'working')+'</b>'+(r.stageLabel?' · '+esc(r.stageLabel):'')+(pct!==null?' · '+Math.round(pct)+'%':'')+(typeof r.etaSec==='number'?' · ~'+Math.round(r.etaSec)+'s left':''));if(r.evidence&&r.evidence.length)renderFrames(r.evidence);if(r.errorCode==='addin_restart_required'){if(!document.querySelector('[data-ask]'))showAsk(r);liveStop('waiting for your answer')}pollTimer=setTimeout(loop,tick<3?5000:10000)}).catch(function(e){var notYet=e.code==='run_not_found'||e.code==='unknown_run_id'||/no prove run/i.test(e.message||'');var transient=notYet||/error sending request|unreachable|HTTP 0|HTTP 5\d\d|timed out|main_thread_busy/i.test(e.message||'');if(transient&&tick<12){setStage('<b>'+(notYet?'starting (the run is not on file yet)':'bridge not answering, retrying')+'</b> · '+esc((e.message||'').slice(0,90))+' (try '+tick+' of 12)');pollTimer=setTimeout(loop,8000);return}liveStop('poll failed');setProg(100,'fail');setStage('<b>poll failed</b> '+esc(e.message));status($('importStatus'),'poll failed','bad')})})()} function showRestartedMidRun(r){$('stage').innerHTML='<b>the bridge restarted under this run</b> '+esc(r.error||'')+' <button class="btn" data-action="start_again" style="min-height:32px;padding:4px 10px;margin-left:8px">Start again</button>';$('stage').querySelector('[data-action=start_again]').onclick=importBundle;toast('The bridge restarted mid-run (an update or a crash). Press Start again.','err')}-function importBundle(){if(!part)return;var b=$('btnImport');b.disabled=true;status($('importStatus'),'working');$('gallery').innerHTML='';$('receipt').innerHTML='';$('lint').innerHTML='';setProg(0);setStage('fetching '+esc(part.lbr.name)+' from the wiki...');var fetches=[fetch(part.lbr.url).then(function(r){return r.arrayBuffer()})].concat(part.steps.map(function(s){return fetch(s.url).then(function(r){return r.arrayBuffer()})}));Promise.all(fetches).then(function(bufs){setStage('zipping in the browser...');var entries=[{name:part.lbr.name,data:bufs[0]}].concat(part.steps.map(function(s,i){return{name:s.name,data:bufs[i+1]}}));var zip=makeZip(entries);var name=part.name.replace(/[^A-Za-z0-9._-]+/g,'_');return stage('~/adom-wiki-libs/'+name+'.zip',zip,'staging the bundle on this machine (approve in Adom Bridge)').then(function(p){staged=p;setStage('installing the bundle (one call)...');return cmd('fusion_install_library_bundle',{zipPath:p,libraryName:name})})}).then(function(d){renderReceipt(d);if(d.errorCode==='nothing_to_bind'){setStage('<b>installed</b>, nothing to bind: '+esc(d._hint||''));status($('importStatus'),'installed, unbound','warn');b.disabled=false;return}if(d.attached)toast('An import is already running; attached to '+d.runId);else toast('Installed and started run '+d.runId,'ok');poll(d.runId)}).catch(function(e){setProg(100,'fail');setStage('<b>'+esc(e.code||'failed')+'</b> '+esc(e.message));status($('importStatus'),'failed','bad');if(e.payload&&e.payload.errorCode==='addin_restart_required')showAsk(e.payload);toast((e.code==='web_denied'?'Denied in Adom Bridge: ':'')+e.message,'err')}).finally(function(){b.disabled=false})}+function importBundle(){if(!part)return;probe().then(function(b){if(b)bridge=b});var b=$('btnImport');b.disabled=true;status($('importStatus'),'working');$('gallery').innerHTML='';$('receipt').innerHTML='';$('lint').innerHTML='';setProg(0);var ps=part.parts||[part];setStage('fetching '+ps.length+' librar'+(ps.length===1?'y':'ies')+(part.steps.length?' and '+part.steps.length+' model(s)':'')+' from the wiki...');var lbrFetches=ps.map(function(p){return fetch(p.lbr.url).then(function(r){return r.text()})});var stepFetches=part.steps.map(function(s){return fetch(s.url).then(function(r){return r.arrayBuffer()})});Promise.all([Promise.all(lbrFetches),Promise.all(stepFetches)]).then(function(res){var texts=res[0],bufs=res[1];setStage((ps.length>1?'merging '+ps.length+' libraries into one and ':'')+'zipping in the browser...');var merged=mergeLbrs(texts);var name=part.name.replace(/[^A-Za-z0-9._-]+/g,'_');var entries=[{name:name+'.lbr',data:new TextEncoder().encode(merged).buffer}].concat(part.steps.map(function(s,i){return{name:s.name,data:bufs[i]}}));var zip=makeZip(entries);return stage('~/adom-wiki-libs/'+name+'.zip',zip,'staging the bundle on this machine (approve in Adom Bridge)').then(function(p){staged=p;setStage('installing the bundle (one call)...');return cmd('fusion_install_library_bundle',{zipPath:p,libraryName:name})})}).then(function(d){renderReceipt(d);if(d.errorCode==='nothing_to_bind'||d.started===false){setProg(100,'done');setStage('<b>installed, unbound</b> · '+esc(d._hint||'nothing to bind'));status($('importStatus'),'installed, unbound','warn');toast('Library placed at '+d.libraryPath+' with no 3D bound','ok');b.disabled=false;return}if(d.attached)toast('An import is already running; attached to '+d.runId);else toast('Installed and started run '+d.runId,'ok');poll(d.runId)}).catch(function(e){setProg(100,'fail');setStage('<b>'+esc(e.code||'failed')+'</b> '+esc(e.message));status($('importStatus'),'failed','bad');if(e.payload&&e.payload.errorCode==='addin_restart_required')showAsk(e.payload);if(e.code==='web_denied'){setStage('<b>not allowed</b> Adom Bridge asked you to allow this page and did not get a click in time. Press the button again and click Allow (1 hr) in the Adom Bridge toast, or grant this page in ab Settings > Web Control.');}toast((e.code==='web_denied'?'Adom Bridge did not get your Allow click. ':'')+e.message,'err')}).finally(function(){b.disabled=false})} function attachRun(){cmd('fusion_prove_result',{}).then(function(d){var r=d.result||d;if(!r.runId){toast('No run to attach to','err');return}toast('Attached to run '+r.runId);poll(r.runId)}).catch(function(e){toast(e.message,'err')})} // ---- the restart ask: always a choice ---------------------------------------------------function showAsk(p){var docs=(p.openDocuments||p.documentNames||[]);var unsaved=p.unsavedDocuments||[];$('ask').innerHTML='<div class="ask" data-ask="addin_restart_required"><h4>Fusion needs a restart to load the Adom add-in</h4><p>'+esc(p.userMessage||'The add-in is not answering. Restarting Fusion loads it. Your documents will be saved first.')+'</p>'+(docs.length?'<ul>'+docs.map(function(d){return '<li>'+esc(typeof d==='string'?d:(d.name||''))+(unsaved.indexOf(typeof d==='string'?d:d.name)>=0?' (unsaved)':'')+'</li>'}).join('')+'</ul>':'<p class="mini">'+(p.documentsKnown===false?'The bridge could not list open documents.':'No open documents.')+'</p>')+'<div class="row"><button class="btn primary" data-action="restart_save">Save and restart Fusion</button><button class="btn" data-action="restart_hold">I\'ll handle it</button></div></div>';$('ask').querySelector('[data-action=restart_save]').onclick=function(){cmd(p.remedyVerb||'fusion_restart_for_addin',{save:true},{timeoutMs:135000}).then(function(){toast('Fusion is restarting with your documents saved','ok');$('ask').innerHTML=''}).catch(function(e){toast(e.message,'err')})};$('ask').querySelector('[data-action=restart_hold]').onclick=function(){$('ask').innerHTML='<div class="mini">You own the restart. Nothing on this page will restart Fusion.</div>';toast('Okay, the restart is yours')}}+function showAsk(p){var docs=(p.openDocuments||p.documentNames||[]);var unsaved=p.unsavedDocuments||[];$('ask').innerHTML='<div class="ask" data-ask="addin_restart_required"><h4>Fusion needs a restart to load the Adom add-in</h4><p>'+esc(p.userMessage||'The add-in is not answering. Restarting Fusion loads it. Your documents will be saved first.')+'</p>'+(docs.length?'<ul>'+docs.map(function(d){return '<li>'+esc(typeof d==='string'?d:(d.name||''))+(unsaved.indexOf(typeof d==='string'?d:d.name)>=0?' (unsaved)':'')+'</li>'}).join('')+'</ul>':'<p class="mini">'+(p.documentsKnown===false?'The bridge could not list open documents.':'No open documents.')+'</p>')+'<div class="row"><button class="btn primary" data-action="restart_save">Save and restart Fusion</button><button class="btn" data-action="restart_hold">I\'ll handle it</button></div></div>';$('ask').querySelector('[data-action=restart_save]').onclick=function(){cmd(p.remedyVerb||'fusion_restart_for_addin',{save:true},{timeoutMs:135000}).then(function(){toast('Fusion is restarting with your documents saved','ok');$('ask').innerHTML=''}).catch(function(e){toast(e.message,'err')})};$('ask').querySelector('[data-action=restart_hold]').onclick=function(){cmd(p.remedyVerb||'fusion_restart_for_addin',{decline:true,notificationId:p.notificationId||''}).catch(function(){});$('ask').innerHTML='<div class="mini">You own the restart. Nothing will restart Fusion until you do; the running import stops.</div>';toast('Okay, the restart is yours')}} // ---- fusion state, plan ------------------------------------------------------------------ function fusionState(){status($('fusionStatus'),'reading...');return cmd('fusion_readiness',{}).then(function(r){var x=r.data||r;var d={installed:x.installed?'yes':'no',running:x.running?'yes':'no',ready:x.ready?'yes':'no','bridge':x.bridgeVersion||'?'};if(x.needsSignin)d['needs sign-in']='yes';kv($('fusionKv'),d);status($('fusionStatus'),x.ready?'ready':(x.running?'running, add-in not ready':(x.installed?'installed, not running':'not installed')),x.ready?'ok':'warn');return cmd('fusion_get_app_state',{})}).then(function(r){var x=r.data||r;var docs=x.documents||[];$('docs').innerHTML=docs.length?docs.map(function(d){return '<li><span>'+esc(d.name||'')+(d.isModified||d.modified?' <span class="mod">unsaved</span>':'')+'</span><span>'+esc(d.isActive?'active':'')+'</span></li>'}).join(''):'<li><span>no documents open</span><span></span></li>'}).catch(function(e){status($('fusionStatus'),e.message,'bad');toast(e.message,'err')})}@@ -270,7 +283,7 @@ function describe(){status($('catStatus'),'loading...');return cmd('fusion_descr function renderCatalog(){var f=$('catFilter').value.toLowerCase();$('catalog').innerHTML=catalog.filter(function(v){return !f||(v.name+' '+v.desc).toLowerCase().indexOf(f)>=0}).slice(0,200).map(function(v){return '<div><b>'+esc(v.name)+'</b> <span>'+esc(String(v.desc).slice(0,160))+'</span></div>'}).join('')||'<div><span>nothing matches</span></div>'} // ---- wire up ----------------------------------------------------------------------------------$('btnConnect').onclick=connect;$('btnResolve').onclick=function(){resolvePart()};$('btnShuffle').onclick=shuffle;$('partRef').addEventListener('keydown',function(e){if(e.key==='Enter')resolvePart()});+$('btnConnect').onclick=connect;$('btnResolve').onclick=function(){delete $('partRef').dataset.multiStart;resolvePart()};$('tglNoModel').onclick=function(){skipModel=!skipModel;this.className='toggle'+(skipModel?' active':'');this.setAttribute('aria-checked',String(skipModel));if(part)resolvePart()};$('btnShuffle').onclick=shuffle;$('partRef').addEventListener('keydown',function(e){if(e.key==='Enter')resolvePart()}); $('btnFusion').onclick=fusionState;$('btnPlan').onclick=plan;$('btnImport').onclick=importBundle;$('btnAttach').onclick=attachRun; $('btnFeed').onclick=function(){feed('status')};$('btnFeedStart').onclick=function(){feed('start')};$('btnFeedStop').onclick=function(){feed('stop')}; $('btnBannerShow').onclick=function(){banner(true)};$('btnBannerHide').onclick=function(){banner(false)};
page.json+7−10@@ -11,10 +11,6 @@ "uninstall": "./uninstall.sh" }, "tags": [- "ab",- "bridge",- "fusion",- "desktop-app", "fusion360", "autodesk", "cad",@@ -79,10 +75,7 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge",- "fusion dashboard",- "fusion status page",- "open the fusion launch page"+ "set up the fusion bridge" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -111,5 +104,9 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ]-}+ ],+ "author": {+ "name": "John Lauer",+ "email": "[email protected]"+ }+}