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
Update 102 file(s)
10 files changed
+94−27
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.268+1.9.269
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.268",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.268/adom-bridge-fusion-v1.9.268.zip",- "sha256": "c48a5696ad0bf60e4fcf9c5f0ae4bef197439bdd20cb12f5c8807c0b6038dddb",- "size": 559131,+ "version": "1.9.269",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.269/adom-bridge-fusion-v1.9.269.zip",+ "sha256": "9cf5296fa030fa306882e4a12ae0d94f5cea81077d78f19c2b31045024e86833",+ "size": 560100, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.268",+ "version": "1.9.269", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.268"+version = "1.9.269" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.268"+version = "1.9.269" edition = "2021" [dependencies]
dashboard/ui/quick-actions.html+6−3@@ -232,9 +232,12 @@ function connect(){status($('connStatus'),'probing 127.0.0.1:24680-24689...');$( // ---- part files ---------------------------------------------------------------------- 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 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 fetch('https://wiki.adom.inc/api/v1/pages/'+ref,{signal:timeout(15000)}).then(function(r){return r.ok?r.json():{}}).catch(function(){return{}}).then(function(pg){var page=pg.page||pg;return{ref:ref,name:ref.split('/').pop(),title:page.title||'',brief:page.brief||'',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).+// A library with no deviceset description or MPN is unusable to an EE; when the wiki page has+// them (its title is the part number, its brief the one-line description) fill the gaps and say so.+function fillPartMeta(text,rec,fills){if(!rec||(!rec.brief&&!rec.title))return text;try{var P=new DOMParser();var d=P.parseFromString(text,'application/xml');var changed=false;[].forEach.call(d.querySelectorAll('library > devicesets > deviceset'),function(ds){var name=ds.getAttribute('name');var desc=ds.querySelector(':scope > description');if((!desc||!(desc.textContent||'').trim())&&rec.brief){if(!desc){desc=d.createElement('description');ds.insertBefore(desc,ds.firstChild)}desc.textContent=rec.brief;fills.push({deviceset:name,field:'description',from:rec.ref});changed=true}[].forEach.call(ds.querySelectorAll('devices > device'),function(dev){var techs=dev.querySelectorAll('technologies > technology');if(!techs.length){var ts=dev.querySelector('technologies');if(!ts){ts=d.createElement('technologies');dev.appendChild(ts)}var t=d.createElement('technology');t.setAttribute('name','');ts.appendChild(t);techs=[t]}[].forEach.call(techs,function(t){var has=[].some.call(t.querySelectorAll('attribute'),function(a){return (a.getAttribute('name')||'').toUpperCase()==='MPN'&&(a.getAttribute('value')||'').trim()});if(!has&&rec.title){var a=d.createElement('attribute');a.setAttribute('name','MPN');a.setAttribute('value',rec.title);a.setAttribute('constant','no');t.appendChild(a);fills.push({deviceset:name,field:'MPN',from:rec.ref});changed=true}})})});return changed?new XMLSerializer().serializeToString(d):text}catch(e){return text}} 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 --------------------------@@ -254,7 +257,7 @@ function stage(path,buf,note){var u=new Uint8Array(buf),CH=1024*1024,n=Math.max( 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'+((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)}+function renderReceipt(d){var unb=(d.unbound||[]).map(function(u){return u.package+' ('+u.why+')'}).join('; ');var fl=(part&&part.metaFills)||[];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','filled from wiki pages':fl.length?fl.map(function(f){return f.deviceset+' '+f.field}).join(', '):'nothing (source libraries were complete)','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())}})})}@@ -269,7 +272,7 @@ function liveTick(){if(!live.on)return;cmd('fusion_live_feed',{action:'frames',s 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'){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;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 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 fills=[];texts=texts.map(function(t,i){return fillPartMeta(t,ps[i],fills)});part.metaFills=fills;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 --------------------------------------------------
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.268"+DASH_VERSION="1.9.269" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.268",+ "version": "1.9.269", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.268",+ "version": "1.9.269", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+67−6@@ -4421,6 +4421,8 @@ def _orchestrate_build_library_3d(args: dict) -> dict: "partsTotal": len(_parts), "partsDone": 0, "parts": []} _publish_run_state(_run_id, _bstate) + _descs = _library_descriptions((args.get("lbrPath") or "").replace("\\", "/"))+ def _on_part(entry, i, total): # PER-PART PROGRESS (Colby, PR #58): a 7-part build was a silent 10 minutes. _bstate["stage"] = "minting %s (%d/%d)" % (entry.get("package") or "part", i, total)@@ -4432,8 +4434,9 @@ def _orchestrate_build_library_3d(args: dict) -> dict: # first watched real run (2026-08-28) it stayed absent for the whole bind stage # because nothing updated it at a yield point. Now a 7-part bind shows # "Binding 3D - minting X (3/7)" as each part lands.- _banner("update", title="Binding 3D",- detail="minting %s (%d of %d)" % (entry.get("package") or "part", i, total),+ _banner("update", title="Binding 3D: part %d of %d" % (i, total),+ detail="%s. Importing its STEP and saving its 3D package"+ % _named(entry.get("package") or "part", _descs), percent=100.0 * i / max(1, total) * 0.4, eta_sec=None) def _run():@@ -4623,6 +4626,33 @@ def _build_library_3d_core(args: dict) -> dict: } ++def _library_descriptions(lbr_path: str) -> dict:+ """{deviceset name -> description, package name -> description} for palette text (John,+ 2026-08-29: "always give me the name/description of the chip so I know what it's for")."""+ out = {}+ try:+ md = _library_part_metadata(lbr_path)+ for ds in md.get("devicesets") or []:+ d = (ds.get("description") or "").strip()+ if d:+ out[str(ds.get("name") or "").lower()] = d+ for v in ds.get("variants") or []:+ if v.get("package"):+ out.setdefault(str(v["package"]).lower(), d)+ except Exception:+ pass+ return out+++def _named(name: str, descs: dict, max_len: int = 70) -> str:+ """'CH340G: USB serial converter, UART...' or just the name when nothing is known."""+ d = (descs or {}).get(str(name or "").lower(), "")+ if not d:+ return str(name or "library")+ d = d if len(d) <= max_len else d[:max_len - 1].rstrip() + "…"+ return "%s: %s" % (name, d)+ def _banner(action, title="", detail="", percent=None, eta_sec=None, tone="working"): """Drive the in-Fusion 'Adom is driving Fusion' palette. Fire-and-forget. @@ -11805,12 +11835,28 @@ def _part_metadata_report(path: str) -> dict: label = {"MPN": "no manufacturer part number", "MANUFACTURER": "no manufacturer", "MOUSER_PN": "no Mouser part number", "DIGIKEY_PN": "no DigiKey part number", "LCSC_PN": "no LCSC part number"}+ # DESCRIPTION (John, 2026-08-29, the Content Manager's empty Description column): a+ # deviceset with no description is as unusable as one with no part number; report it+ # in the same breath. The page fills it from the wiki brief when the library lacks it.+ n_no_desc = sum(1 for ds in md["devicesets"] if not (ds.get("description") or "").strip())+ missing_counts["DESCRIPTION"] = n_no_desc+ for ds in md["devicesets"]:+ if not (ds.get("description") or "").strip():+ hit = next((c for c in comps if c["component"] == ds["name"]), None)+ if hit:+ hit["missing"].append("a description")+ else:+ comps.append({"component": ds["name"], "variants": len(ds["variants"] or []),+ "missing": ["a description"], "absentDistributors": [],+ "variantNames": [v["fullName"] for v in (ds["variants"] or [])]}) def _n(n, what): return "%d of %d component%s ha%s %s" % (n, total, "" if total == 1 else "s", "s" if n == 1 else "ve", what) bits = [_n(missing_counts[f], label[f]) for f in ("MPN", "MANUFACTURER") if missing_counts[f]]+ if n_no_desc:+ bits.append(_n(n_no_desc, "no description")) if n_no_distributor: bits.append(_n(n_no_distributor, "no distributor part number at all")) return {@@ -12670,9 +12716,13 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # read "captured symbol (5 of 4)" - a narration line the front end renders # under the progress bar, reading nonsense from part two onward. _tot = 4 * max(1, int(state.get("partsToProve") or state.get("partsTotal") or 1))- state["stageLabel"] = "captured %s (%d of %d)" % (entry.get("step"), len(ev), _tot)- _banner("update", title="Proving %s" % (entry.get("part") or "library"),- detail=state["stageLabel"],+ state["stageLabel"] = ("part %s of %s: captured %s (frame %d of %d)"+ % (state.get("partIndex") or "?", state.get("partsToProve") or "?",+ entry.get("step"), len(ev), _tot))+ _banner("update", title="Proving part %s of %s" % (state.get("partIndex") or "?",+ state.get("partsToProve") or "?"),+ detail="%s. Captured %s (frame %d of %d)" % (_named(entry.get("part") or "library", _descs_p),+ entry.get("step"), len(ev), _tot), percent=100.0 * (_est_done[0] + _est_by_stage.get("prove", 0.0) * len(ev) / _tot) / _est_total, eta_sec=max(0, _est_total - _est_done[0]@@ -12692,13 +12742,24 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: except Exception: _all_devs = [] _targets = [_want] if _want else (_all_devs or [""])+ _descs_p = _library_descriptions(target_lbr) state["partsToProve"] = len(_targets) state["provedParts"] = [] _prove_results, _all_ok = [], True for _pi, _dev in enumerate(_targets, 1):- state["stageLabel"] = ("proving %s (%d of %d)"+ state["stageLabel"] = ("proving %s (part %d of %d)" % (_dev or "library", _pi, len(_targets)))+ state["partIndex"] = _pi _save()+ # The palette must say WHERE we are before the first frame lands (John,+ # 2026-08-29: "10 parts" with no position "leaves me, the human, a bit lost").+ _banner("update", title="Proving part %d of %d" % (_pi, len(_targets)),+ detail="%s. Opening its symbol, footprint, 3D package and ganged view"+ % _named(_dev or "library", _descs_p),+ percent=100.0 * (_est_done[0] + _est_by_stage.get("prove", 0.0)+ * (_pi - 1) / max(1, len(_targets))) / _est_total,+ eta_sec=max(0, _est_total - _est_done[0]+ - _est_by_stage.get("prove", 0.0) * (_pi - 1) / max(1, len(_targets)))) def _frame_for(entry, _d=_dev): entry = dict(entry)