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 15 file(s)
10 files changed
+104−126
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.201+1.9.202
adom-bridge-fusion-manifest.json+3−3@@ -1,9 +1,9 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.201",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.201/adom-bridge-fusion-v1.9.201.zip",- "sha256": "2f4f3efa81d1cfb1712a101157e489d98a7fa965cef5fb039c401f3078402e7c",+ "version": "1.9.202",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.202/adom-bridge-fusion-v1.9.202.zip",+ "sha256": "eba8083cf16b8a6001c6c4a7cf8f9424ae3c78d9fc2ff738bf873391a0bbf11c", "size": 495989, "verbPrefixes": [ "fusion_"
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.201",+ "version": "1.9.202", "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.201"+version = "1.9.202" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.201"+version = "1.9.202" edition = "2021" [dependencies]
dashboard/src/main.rs+50@@ -42,6 +42,13 @@ const FAVICON_PNG: &[u8] = include_bytes!("../ui/fusion-icon.png"); /// The app mark (icons contract, 2026-08-24): ONE line-drawing SVG for the tab favicon, /// the header mark, the dock card and the wiki page. Never Autodesk's filled badge. const FAVICON_SVG: &[u8] = include_bytes!("../../docs/img/dock-fusion.svg");+/// The SHARED newbie panel (adom-ui-design 2.0.0), vendored VERBATIM. Never reformat it:+/// it builds its noun-highlight with RegExp() rather than a literal precisely because a+/// page assembled as a template literal turns \b into a BACKSPACE and eats the word+/// boundaries silently - the regex still matches, just too greedily, so nothing looks+/// broken. Seven apps hand-rolled this panel and produced four paddings, two border+/// colours and three Copy drawings; this file is the fix for that class of drift.+const NEWBIE_PANEL_JS: &[u8] = include_bytes!("../ui/vendor/newbie-panel.js"); fn ok(msg: &str) { println!("OK: {}", msg);@@ -1105,6 +1112,49 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { ("GET", "/") | ("GET", "/index.html") => { respond_nostore(&mut stream, "text/html; charset=utf-8", UI_HTML.as_bytes()); }+ ("GET", "/newbie-panel.js") => {+ respond(&mut stream, "200 OK", "application/javascript; charset=utf-8", NEWBIE_PANEL_JS);+ }+ // SAME-ORIGIN PROXY for the module's two calls. The module defaults its apiBase to+ // '' and fetches these paths on the page's own origin, which is right: the page+ // cannot know the control API's dynamic port, and the server already resolves it.+ ("GET", "/updater/status") => {+ match control_api_base() {+ Some(base) => {+ let out = Command::new("curl")+ .args(["-s", "-m", "8", &format!("{}/updater/status", base)]).output();+ match out {+ Ok(o) if !o.stdout.is_empty() =>+ respond(&mut stream, "200 OK", "application/json", &o.stdout),+ _ => respond_json(&mut stream, &serde_json::json!({"ok": false,+ "why": "control API did not answer /updater/status"})),+ }+ }+ None => respond_json(&mut stream, &serde_json::json!({"ok": false,+ "why": "workspace control API not found"})),+ }+ }+ ("POST", "/ai-threads/run-prompt") => {+ match control_api_base() {+ Some(base) => {+ // 120s: opening drives the editor and CDP touches serialize through one+ // gate, so calls QUEUE rather than fail. A short timeout here would+ // manufacture the phantom failure whose retry double-sends.+ let out = Command::new("curl")+ .args(["-s", "-m", "120", "-X", "POST", "-H", "Content-Type: application/json",+ "-d", &body.to_string(), &format!("{}/ai-threads/run-prompt", base)])+ .output();+ match out {+ Ok(o) if !o.stdout.is_empty() =>+ respond(&mut stream, "200 OK", "application/json", &o.stdout),+ _ => respond_json(&mut stream, &serde_json::json!({"ok": false,+ "why": "control API did not answer run-prompt within 120s"})),+ }+ }+ None => respond_json(&mut stream, &serde_json::json!({"ok": false,+ "why": "workspace control API not found"})),+ }+ } ("GET", "/favicon.svg") => { respond(&mut stream, "200 OK", "image/svg+xml", FAVICON_SVG); }
dashboard/ui/index.html+34−109@@ -54,32 +54,6 @@ header{height:44px;display:flex;align-items:center;gap:12px;padding:0 16px;backg .iconbtn:hover{background:var(--elevated);border-color:var(--border)} .iconbtn:active{transform:scale(.9);background:var(--overlay)} main{max-width:1080px;margin:0 auto;padding:20px 16px 60px}-/* say-strip contract (adom-ui-design 1.9): ONE strip fleet-wide */-.say{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--teal);border-radius:10px;padding:14px 16px;margin-bottom:18px;position:relative}-.say h2{font:600 15px var(--sans);margin:0;color:var(--text)}-.say-head p{color:var(--muted);font-size:13.5px;margin:4px 0 0}-.say-head p .noun{color:var(--teal)}-.say-x{position:absolute;top:8px;right:10px;background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;padding:2px 6px;border-radius:6px}-.say-x:hover{color:var(--text);background:var(--elevated)}-.say-chips{margin-top:6px}-.say-chip{display:inline-flex;align-items:center;gap:8px;background:var(--surface2);border:1px solid var(--line);border-radius:999px;padding:5px 6px 5px 12px;font-size:13.5px;margin:4px 6px 0 0;cursor:pointer;color:var(--text-body)}-.say-chip em{color:var(--teal);font-style:normal}-.say-chip .copy,.say-chip .send{font:500 11px var(--body);background:none;border:1px solid var(--line);border-radius:999px;padding:2px 9px;cursor:pointer}-.say-chip .copy{color:var(--muted)}-.say-chip .send{color:var(--teal);border-color:rgba(0,184,177,.45)}-.say-chip:hover .copy{color:var(--teal);border-color:var(--teal)}-.say-chip .send:hover{background:rgba(0,184,177,.12)}-/* dash-demo player toolbar: a remote control, not a feature */-#tourTransport{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:9500;display:flex;align-items:center;gap:6px;background:var(--overlay);border:1px solid var(--line);border-radius:999px;padding:6px 12px;box-shadow:0 8px 24px rgba(0,0,0,.45)}-.tt{background:none;border:none;color:var(--text);font-size:15px;cursor:pointer;padding:4px 8px;border-radius:8px;transition:background .15s ease,transform .06s ease}-.tt:hover{background:var(--elevated)}-.tt:active{transform:scale(.92)}-.tt.muted{color:var(--muted)}-.tt-exit{color:var(--red);font-size:12.5px}-.tt-sep{width:1px;height:16px;background:var(--line)}-.tt-step{font:12px var(--mono);color:var(--muted);padding:0 2px}-.say-folded{font-size:13px;color:var(--muted);cursor:pointer}-.say-folded:hover{color:var(--teal)} h2{font-family:'Familjen Grotesk',sans-serif;font-size:17px;color:var(--text);margin:26px 0 12px} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px} .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px}@@ -277,13 +251,10 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0} <!-- SAY IT (dock-bar `say` contract, John 2026-08-23): teach the ASK before the status. "for fusion I might say 'open the step in fusion'... those should all be taught first and foremost in those dashboards, possibly even at the top of it." -->- <section class="say" id="saystrip">- <div class="say-head"><h2>Say it</h2>- <p>You never need this page's buttons: just tell your AI what you want, using the word <b class="noun">fusion</b>. Copy a phrase, or send it straight to a new AI thread.</p>- <button class="say-x" id="sayDismiss" aria-label="Collapse" data-tip="Fold this - the ? in the header brings it back">×</button></div>- <div class="say-chips" id="sayChips"></div>- <div class="say-folded" id="sayFolded" style="display:none">Say it - <span id="sayCount">6</span> phrases</div>- </section>+ <!-- SHARED newbie panel (adom-ui-design 2.0.0). The module owns the card, columns,+ table, both icons, clipboard + send paths, toasts, tooltips, X and ? toggle.+ We own only: our phrases, our noun, and what the demo button does. -->+ <div id="newbie"></div> <h2>Status</h2> <div id="targets"></div>@@ -296,15 +267,10 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0} <button class="btn" id="btnStop" data-tip="Close Fusion gracefully on the selected box (documents are closed cleanly first)">Close Fusion</button> </div> - <div id="demosFolded" style="display:none;font-size:13px;color:var(--muted);cursor:pointer;margin:14px 0" data-tip="Tap to show the demos once (the header ? restores everything)">Demos - guided tour and part import</div>- <div id="demosRegion"> <h2>Demos</h2> <div class="card"> <div class="led-label">Guided tour</div>- <div class="led-sub">Walks every surface on a sample part: library, symbol, footprint, 3D chip, a generated solo board in 2D and 3D. Runs entirely in the background.</div>- <div class="btnrow" style="align-items:center">- <button class="btn primary" id="btnTour" data-tip="Walk every layer of the bridge - library, symbol, footprint, the 3D chip, Data Hub, APS search, MCP, a generated board in 2D and 3D, and real exports - narrated, captioned, with a live progress bar">Play the demo</button>- </div>+ <div class="led-sub">Walks every surface on a sample part: library, symbol, footprint, 3D chip, a generated solo board in 2D and 3D. Runs entirely in the background. Start it from <b>Play the demo</b> at the top of the page - the shared panel owns that button, so there is exactly one of it.</div> <div class="barlabel" id="tourCaption" style="display:none;margin-top:8px;color:var(--text);font-size:13px;line-height:1.5"></div> <div class="bar" id="tourBar"><div></div></div> <div class="barlabel" id="tourLabel"></div>@@ -336,7 +302,6 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0} <div class="led-sub">Coming next: pulls a molecule page and opens its model in Fusion. Ask your AI: "open the molecule from wiki page owner/slug in Fusion".</div> </div> - </div><!-- /demosRegion --> <h2>Surfaces: every content type, one real part</h2> <div class="seatnote" id="surfnote">Loading the demo part...</div>@@ -345,6 +310,22 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0} <h2>Ask your AI</h2> <div id="prompts"></div> </main>+<script src="newbie-panel.js"></script>+<script>+ // OUR WHOLE SURFACE: the phrases, the trigger noun, and what the demo button does.+ // Everything visual - card, columns, table, icons, clipboard, send path, toasts,+ // tooltips, X, ? toggle - belongs to the module, on purpose.+ AdomNewbiePanel.mount({+ mount: '#newbie',+ app: 'fusion',+ appName: 'Fusion Dashboard',+ slug: 'fusion-bridge', // dock:<wiki page slug>, never the dock card id+ toggleButton: '#btnRecall',+ demo: { label: 'Play the demo', meta: 'the whole bridge, about 10 minutes',+ onPlay: function () { startFusionDemo(); } },+ phrases: SAY_PHRASES+ });+</script> <dialog id="dlgSettings" class="settings" tabindex="-1" aria-label="Fusion Dashboard Settings"> <div class="dlghead">@@ -527,71 +508,12 @@ function copyText(t){ return Promise.resolve(copyTextLegacy(t)); } function copyTextLegacy(t){const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');}catch(e){}ta.remove();}-(function renderSayChips(){- // say-strip contract: TWO buttons per chip - Copy (ghost, fleet toast) and Send to AI- // (teal outline, NEW thread via POST /api/say-send; on any failure copy + error toast- // so the user is never stranded). Clicking the phrase itself also copies.- const host=$('#sayChips');- $('#sayCount').textContent=SAY_PHRASES.length;- const noun=/(fusion)/i;- SAY_PHRASES.forEach(p=>{- const chip=document.createElement('span');chip.className='say-chip';chip.dataset.say=p;- const quoted='\u201C'+p.replace(noun,'<em>$1</em>')+'\u201D';- chip.innerHTML=quoted+' <button class="copy">Copy</button><button class="send">Send to AI</button>';- const doCopy=async()=>{await copyText(p);toast('Copied to clipboard. Paste it into any AI thread.','ok');};- chip.addEventListener('click',doCopy);- chip.querySelector('.copy').addEventListener('click',e=>{e.stopPropagation();doCopy();});- chip.querySelector('.send').addEventListener('click',async e=>{- e.stopPropagation();- const r=await fetch('api/say-send',{method:'POST',headers:{'Content-Type':'application/json'},- body:JSON.stringify({phrase:p})}).then(x=>x.json()).catch(()=>null);- if(r&&r.ok){toast('Sent to a new AI thread.','ok');gradSave({aiDriven:true});applyGraduation();}- else{await copyText(p);toast((r&&r.why)||'Could not reach the AI panel; copied instead.','err');}- });- host.appendChild(chip);- });- $('#sayDismiss').onclick=()=>{gradSave({dismissed:{...(gradState().dismissed||{}),say:true}});applyGraduation();};- $('#sayFolded').onclick=()=>{$('#sayChips').style.display='';$('#sayFolded').style.display='none';- document.querySelector('.say-head p').style.display='';};-})(); -// ── newbie-graduation: Fresh / Graduated / Recallable, localStorage per-app origin ──-const GRAD_KEY='adom-dash-graduation';-function gradState(){try{return JSON.parse(localStorage.getItem(GRAD_KEY))??{}}catch(e){return{}}}-function gradSave(patch){try{localStorage.setItem(GRAD_KEY,JSON.stringify({...gradState(),...patch}))}catch(e){}}-let gradRecalled=false; // the header ? restores Fresh for this session only-function applyGraduation(){- const g=gradState();- const launches=g.launches||0;- const demosFold=!gradRecalled&&((g.dismissed&&g.dismissed.start)||launches>=3||g.aiDriven);- const sayFold=!gradRecalled&&((g.dismissed&&g.dismissed.say)||launches>=5||g.aiDriven);- // the say strip folds LAST: it is the graduation path itself- const strip=$('#saystrip');- if(sayFold){$('#sayChips').style.display='none';$('#sayFolded').style.display='';- document.querySelector('.say-head p').style.display='none';}- else{$('#sayChips').style.display='';$('#sayFolded').style.display='none';- document.querySelector('.say-head p').style.display='';}- const demos=$('#demosRegion');- if(demos)demos.style.display=demosFold?'none':'';- const demosFolded=$('#demosFolded');- if(demosFolded)demosFolded.style.display=demosFold?'':'none';-}-let _gradCounted=false;-function gradCountLaunch(st){- // a launch counts as successful when the status rollup is green within the first minute- if(_gradCounted)return;- const leds=(st&&st.leds)||{};- const core=['fusionRunning','addinReady'];- if(core.every(k=>leds[k]&&leds[k].on)&&(performance.now()<60000)){- _gradCounted=true;gradSave({launches:(gradState().launches||0)+1});- }-}-document.addEventListener('DOMContentLoaded',()=>{applyGraduation();- const df=$('#demosFolded');if(df)df.onclick=()=>{$('#demosRegion').style.display='';df.style.display='none';};});-// the header ? recalls all scaffolding for this session (newbie-graduation: Recallable)-document.addEventListener('DOMContentLoaded',()=>{- $('#btnRecall').onclick=()=>{gradRecalled=true;applyGraduation();toast('Helpers are back for this session - they fold again on reload','ok');};-});++// Scaffolding state - the X, the header ? toggle, and the open/closed memory - is OWNED BY+// THE MODULE now; it is handed our #btnRecall as toggleButton. We deliberately keep no+// parallel copy, because two implementations of "is the panel open" is precisely the drift+// the shared module exists to end. let targetsCache=[]; function statusUrl(){return 'api/status'+(curTarget?('?target='+encodeURIComponent(curTarget)):'');}@@ -653,7 +575,6 @@ function renderLeds(st){ $('#bpBridge').textContent=st.bridgeVersion||window.__lastBridgeVer||'-'; $('#bpAddin').textContent=(leds.addinReady&&leds.addinReady.version)||'-'; $('#meta').textContent=st.stale?'(busy - last verified state)':'';- gradCountLaunch(st); } async function refresh(){try{lastStatus=await api(statusUrl());renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}} $('#btnRefresh').onclick=()=>{toast('refreshing status');refresh();};@@ -661,7 +582,7 @@ $('#btnStart').onclick=async()=>{toast('starting Fusion in the background');awai $('#btnStop').onclick=async()=>{toast('closing Fusion');await act('stop');setTimeout(refresh,4000);}; // Tour with a REAL progress bar (the harness contract: percent / stepLabel / done).-$('#btnTour').onclick=async()=>{+async function startFusionDemo(){ toast('starting the guided tour (background)'); const s=await act('tour');const d=s.data||s;const id=d.demoId; if(!id){toast('tour failed to start: '+(d.error||'unknown'),'err');return;}@@ -722,7 +643,6 @@ $('#btnTour').onclick=async()=>{ if(q.done){ $('#tourTransport').style.display='none'; if(tourAudioEl)tourAudioEl.pause();tourAudioQueue=[];- if(!q.failed){gradSave({aiDriven:true});} $('#tourLabel').textContent=(q.failed?'tour stopped: '+(q.error||''):'tour complete')+' - '+shown+' views captured'; toast(q.failed?('tour failed: '+(q.error||'')):'tour complete: '+shown+' views captured',q.failed?'err':'ok'); return;@@ -958,7 +878,10 @@ const PROMPTS=[ $('#prompts').innerHTML=PROMPTS.map(p=>`<div class="prompt"><code>${p}</code><button class="btn copy" data-tip="Copy this prompt so you can paste it to your AI" onclick="navigator.clipboard.writeText('${p.replace(/'/g,"\\'")}');toast('copied','ok')">Copy</button></div>`).join(''); // ── Live channel (SSE): state pushes, AI toasts, CLI click directives ──-const CLICK_TARGETS={setup:'#btnSetup',start:'#btnStart',stop:'#btnStop',tour:'#btnTour','import-part':'#btnImportPart',settings:'#btnSettings',refresh:'#btnRefresh'};+const CLICK_TARGETS={setup:'#btnSetup',start:'#btnStart',stop:'#btnStop','import-part':'#btnImportPart',settings:'#btnSettings',refresh:'#btnRefresh'};+// `ui click tour` used to press #btnTour; the shared panel owns that button now, so drive+// the demo directly. The CLI contract is unchanged for callers.+const CLICK_FNS={tour:()=>startFusionDemo()}; let sseUp=false; function connectSSE(){ const es=new EventSource('events');@@ -983,7 +906,9 @@ function connectSSE(){ es.addEventListener('toast',e=>{try{const t=JSON.parse(e.data);toast(t.message,t.kind==='info'?'':t.kind);}catch(_){}}); es.addEventListener('click',e=>{try{ const {target}=JSON.parse(e.data);const sel=CLICK_TARGETS[target];- if(sel&&$(sel)){$(sel).click();}else{toast('unknown click target: '+target,'err');}+ if(sel&&$(sel)){$(sel).click();}+ else if(CLICK_FNS[target]){CLICK_FNS[target]();}+ else{toast('unknown click target: '+target,'err');} }catch(_){}}); es.onerror=()=>{sseUp=false;es.close();setTimeout(connectSSE,3000);}; }
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.201"+DASH_VERSION="1.9.202" 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.201",+ "version": "1.9.202", "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.201",+ "version": "1.9.202", "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]"- }-}+ ]+}