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
1.9.133: library mint works without APS (defer the no_work_folder gate to the upload fallback), survive a sick data API (guarded Main-by-name fallback), wait for async first-save lineage, prove marks unobservable thumbnail as unverified not failed, AUTH-001 hint learns the developer-hub dead end (#93)
3 files changed
+111−26
CHANGELOG.md+26@@ -3,6 +3,32 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.9.133 - 2026-08-21 (library mint works without APS)++- **fusion_create_library / build_library_3d no longer require an APS sign-in.** The mint's+ primary path has saved the f3d straight to the cloud through the add-in (doc.saveAs into the+ 'Adom AI Workspace' DataFolder) since 2026-08-15, but an up-front _discover_upload_target gate+ still hard-failed the whole chain with no_work_folder whenever APS could not resolve the+ workspace folder - killing every chain on a box with no APS auth even though the happy path+ never used it. The gate now lives at the APS upload FALLBACK, where APS is actually needed.+ Found live on a fresh install where AUTH-001 (the shared app lost its Data Management API+ product) made APS sign-in impossible (adom-bridge#93); with this change the full chain proved+ a library (4/4 frames) on that box with zero APS.+- **The mint's saveAs script survives a sick data API.** app.data.activeProject can raise+ InternalValidationError (id.size()) on some installs, and individual DataProject property+ reads can too. The script now falls back to resolving the default upload project by id then+ by name 'Main', with per-project guards so one poisoned project cannot abort the search.+- **First-ever cloud saves sync asynchronously.** Right after doc.saveAs on a fresh install,+ dataFile.id can still be the local cache path; the lineage urn appears when the upload lands.+ The script now waits (bounded) for the dm.lineage id instead of misreading the local path as+ a failed save.+- **The prove verdict distinguishes 'cannot check' from 'checked and absent'.** Without APS the+ cloud thumbnail state is unobservable; that now surfaces as thumbnailUnverified:+ aps_unavailable (with a note) instead of failing an otherwise 4/4-proven run.+- **AUTH-001 recovery hint learns the developer-hub dead end.** When the signing-in user is not+ a member of the APS developer hub, myapps shows no app and no portal driving from that account+ can help; the hint now says to check that FIRST and route the fix to the hub admin.+ ## 1.9.76 - 2026-08-18 (ralph, dashboard) - **"Fusion is opening right now" LED (John's ask).** A launch takes 30-90s and the board used to
aps.py+9−10@@ -119,16 +119,15 @@ def _aps_auth001_recovery_hint(client_id: str) -> str: "Allow') - nbrowser_navigate the session to the new authUrl, then nbrowser_click 'Allow'; the URL " "flips to localhost:<port>/fusion-aps/callback?code=... and the bridge captures the token. " "(5) Poll fusion_aps_status until signedIn:true. Verified live 2026-07-14. "- "⚠ STEP ZERO, before driving anything (adom-bridge#93, Colby on a fresh box, "- "2026-08-21): CHECK THE ACCOUNT CAN SEE THE APP AT ALL. Open "- "https://aps.autodesk.com/myapps/ - if it says 'you don't have a hub yet' or the "- "app is not listed, this signed-in user is NOT a member of the app owner's APS "- "DEVELOPER hub (being in the shared DATA hub does not grant it), and NO amount of "- "portal driving from this account can fix AUTH-001. STOP, do not burn the user's "- "time: file/route the re-commit to the APP OWNER (the account that created this "- "Client ID - for the shared adom app that is the adom org owner), and tell the "- "user their box will work the moment the owner saves the API Access checkbox - "- "no action on their side."+ "⚠ FIRST CHECK before driving anything: open myapps AS THIS USER - if it shows "+ "'It seems you don't have a hub yet', this Autodesk account is NOT in the APS "+ "developer hub that owns the app, the app is invisible to it, and NO portal "+ "driving from this account can fix AUTH-001 (dead-ended live 2026-08-21, "+ "adom-bridge#93). Route the fix to the app's owner (the developer-hub admin), "+ "and have them add this user to the developer hub so the next AUTH-001 is "+ "self-servable. Meanwhile the library mint works WITHOUT APS since 1.9.133 "+ "(the add-in saves the f3d straight to the cloud); APS stays needed only for "+ "cloud search and thumbnail verification." )
server.py+76−16@@ -3476,12 +3476,15 @@ def _orchestrate_make_3d_package(args: dict) -> dict: if not lbr_path or not model_path: return {"success": False, "error": "lbrPath (.lbr with the footprint) and modelPath (.step) are required (Windows paths)."}+ # No APS workspace folder (typically: APS not signed in) is NOT fatal here. The primary+ # path below saves the f3d STRAIGHT to the cloud through the add-in (doc.saveAs into a+ # workspace DataFolder the script creates itself), so APS is only needed at the upload+ # FALLBACK - the gate lives there now. The old up-front gate failed every chain on an+ # APS-less box whose happy path never touched APS (adom/adom-bridge#93, 2026-08-21).+ aps_folder_note = None if not folder_id:- return {"success": False, "errorCode": "no_work_folder",- "error": "Could not resolve a non-root 'Adom AI Workspace' upload folder.",- "_hint": "The bridge refuses to write to a shared project ROOT. Sign in (fusion_aps_signin) "- "so it can create 'Adom AI Workspace', or pass a real (non-root) folderId. NEVER "- "pass a project root folderId. See the fusion-cloud-hygiene skill."}+ aps_folder_note = ("APS has no 'Adom AI Workspace' folder (fusion_aps_signin not done?) - "+ "relying on the add-in's direct cloud save; the APS upload fallback is unavailable.") cap_label = args.get("captureLabel") # when set, capture BEFORE (footprint) + AFTER (chip placed) task = _safe_folder_name(args.get("task") or "AI 3D packages") # saveAs subfolder (never root) orient = args.get("orient", True)@@ -3535,16 +3538,51 @@ def _orchestrate_make_3d_package(args: dict) -> dict: + orient_block + "try:\n app.activeViewport.fit()\nexcept: pass\n" # FOLDER HYGIENE: saveAs into an 'Adom AI Workspace'/<task> subfolder, NEVER the project root.- "proj=app.data.activeProject; rf=proj.rootFolder\n"+ # activeProject is None on a fresh box until the user touches the data panel, so fall+ # back to the default upload project by id, then by name 'Main' (mirrors the add-in's+ # _resolve_project; hub->project listing only, never a recursive file walk). "def _sub(p,nm):\n" " for i in range(p.dataFolders.count):\n" " if p.dataFolders.item(i).name==nm: return p.dataFolders.item(i)\n" " return p.dataFolders.add(nm)\n"- f"wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n" "res={}\n"+ "proj=None\n"+ "try: proj=app.data.activeProject\n"+ "except Exception as e: res['projErr']=str(e)[:60]\n"+ # Per-item guards are load-bearing: on some installs individual DataProject+ # property reads raise InternalValidationError (id.size()), and one poisoned+ # project must not abort the search for a healthy one.+ "if not proj:\n"+ " try:\n"+ " hubs=app.data.dataHubs\n"+ " for hi in range(hubs.count):\n"+ " ps=hubs.item(hi).dataProjects\n"+ " for pj2 in range(ps.count):\n"+ " p2=ps.item(pj2)\n"+ " ok=False\n"+ f" try: ok=(p2.id=='{_DEFAULT_UPLOAD_PROJECT}')\n"+ " except Exception: ok=False\n"+ " if not ok:\n"+ " try: ok=(p2.name=='Main')\n"+ " except Exception: continue\n"+ " if ok: proj=p2; break\n"+ " if proj: break\n"+ " except Exception as e: res['projErr']=str(e)[:60]\n" "try:\n"+ " rf=proj.rootFolder\n"+ f" wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n" f" doc.saveAs('{name}', wsf, '3d package (footprint+chip)', '')\n"- " res['path']=doc.dataFile.id if doc.dataFile else None\n"+ # A first-ever cloud save on a fresh install syncs ASYNCHRONOUSLY: right after+ # saveAs, dataFile.id can still be the local cache path; the lineage urn appears+ # once the upload lands. Wait for it (bounded well under the 120s script timeout).+ " import time as _tt\n"+ " fid=None\n"+ " for w in range(40):\n"+ " try: fid=doc.dataFile.id if doc.dataFile else None\n"+ " except Exception: fid=None\n"+ " if fid and 'dm.lineage:' in str(fid): break\n"+ " adsk.doEvents(); _tt.sleep(1.5)\n"+ " res['path']=fid\n" " res['dims']=[round(dx*10,2),round(dy*10,2),round(dz*10,2)]\n" "except Exception as e: res['err']=str(e)[:80]\n" "result=res\n"@@ -3582,6 +3620,15 @@ def _orchestrate_make_3d_package(args: dict) -> dict: if isinstance(r, dict) and r.get("error") else ""), "before": before_shot, "after": after_shot, "_hint": "Confirm modelPath is a valid Windows .step path and Fusion is running."}+ if not folder_id:+ return {"success": False, "errorCode": "no_work_folder",+ "error": "In-app cloud save failed (" + str(res)[:120] + ") and APS has no "+ "'Adom AI Workspace' folder to upload the local f3d to.",+ "before": before_shot, "after": after_shot,+ "_hint": "Two independent paths can mint the 3D package: the add-in's direct cloud "+ "save (needs Fusion running + signed in; the res/projErr above says why it "+ "failed) or the APS upload fallback (needs fusion_aps_signin). Fix either. "+ "The bridge never writes to a shared project ROOT."} up = aps.handle_upload({"projectId": project_id, "folderId": folder_id, "localPath": f3d_path, "fileName": f3d_name}) up_d = up.get("data", up) if isinstance(up, dict) else {}@@ -3720,12 +3767,9 @@ def _build_library_3d_core(args: dict) -> dict: import os as _os2 task = args.get("task") or _os2.path.basename(combined).rsplit(".", 1)[0] project_id, folder_id = _discover_upload_target({**args, "task": task})- if not folder_id:- return {"success": False, "errorCode": "no_work_folder",- "error": "Could not resolve a non-root 'Adom AI Workspace' upload folder.",- "_hint": "The bridge refuses to write to a shared project ROOT. Sign in "- "(fusion_aps_signin) so it can create the workspace folder, or pass a real "- "(non-root) folderId. See the fusion-cloud-hygiene skill."}+ # A missing APS workspace folder no longer gates the build: each part's mint saves+ # straight to the cloud through the add-in and only needs APS as its upload fallback+ # (adom/adom-bridge#93). folder_id=None flows down and the mint gates itself. results = []; bindings = []; shots = [] for p in parts: pkg = p.get("package")@@ -9586,14 +9630,24 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict: # while every local check passes. Cloud state, not pixels; waits up to 3 min because # Fusion renders it asynchronously after a bind. thumb = None+ thumb_unverified = None if artifacts["package3d"]["present"]: try: thumb = _handle_check_3d_thumbnail(fusion_info, {"libraryPath": lbr, "waitSeconds": 180}) artifacts["package3d"]["thumbnail"] = (thumb.get("thumbnail") or {}).get("state") if not thumb.get("success"):- missing.append("thumbnail(%s)" % ((thumb.get("thumbnail") or {}).get("state")- or thumb.get("errorCode") or "unknown"))+ if thumb.get("errorCode") == "no_work_folder":+ # APS is not signed in on this box, so the cloud thumbnail state CANNOT+ # be observed - that is "unverified", not "checked and absent". Autodesk+ # renders the thumbnail from Fusion's own upload regardless of our APS+ # app, so do not fail the whole verdict over an unobservable check+ # (adom/adom-bridge#93); surface it loudly instead.+ thumb_unverified = "aps_unavailable"+ artifacts["package3d"]["thumbnail"] = "unverified(aps_unavailable)"+ else:+ missing.append("thumbnail(%s)" % ((thumb.get("thumbnail") or {}).get("state")+ or thumb.get("errorCode") or "unknown")) except Exception as _e: missing.append("thumbnail(check-failed)") captured = [e for e in evidence if e.get("ok") and e.get("screenshot")]@@ -9608,6 +9662,12 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict: out = {"success": proven, "proven": proven, "libraryPath": lbr, "deviceset": want, "artifacts": artifacts, "missing": missing, "evidence": evidence, "imagesCaptured": len(captured), "imagesRequired": 4}+ if thumb_unverified:+ out["thumbnailUnverified"] = thumb_unverified+ out["_thumbnailNote"] = ("The cloud thumbnail could not be observed (APS not signed in "+ "on this box) - the 4-frame local evidence stands, but verify "+ "the Library Manager preview by eye or sign in APS and run "+ "fusion_check_3d_thumbnail.") # RESOLVE OUR OWN DOCUMENT STATE BEFORE RETURNING (deterministic-close rule): the four # views dirtied the library document; closing it here, verified, means no Save dialog # can ever be raised at the user for this run's scratch work. The .lbr ON DISK is the