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
v1.9.20: make_test_project + screenshot_fusion catalog
6 files changed
+194−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.19+1.9.20
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.19",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.19/adom-bridge-fusion-v1.9.19.zip",- "sha256": "88cc7d1c6bdb87b84a5e4f51672221a3eee2ae83f5162d86dcbc107f7ccafe95",- "size": 361294,+ "version": "1.9.20",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.20/adom-bridge-fusion-v1.9.20.zip",+ "sha256": "ad0886d59240463f882dca229661e046b5d274052cf5e4f0d0e9f8fd5594d442",+ "size": 364613, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.19",+ "version": "1.9.20", "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.",
describe.py+2@@ -202,6 +202,8 @@ _T = [ ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}), ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots}. Poll about once a second and render a REAL progress bar. When done:true the shots array carries base64 images (pass imagesAs:paths to skip the encoding).", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list"}, 15, None, False, {"demoId": "tour-abc123"}), + ("fusion_make_test_project", "Create a NEW electronics test project FROM SCRATCH around a library part - the Web Control demo's PROJECT button, and the answer to 'a customer only has a component page'. Generates a minimal test circuit server-side (the part + a generated 330R series resistor with VCC/GND stubs on a small board) as real EAGLE .sch/.brd, then imports the pair as a new electronics design. NO cloud, NO sign-in, no pre-existing content. After it, fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board all work on the result. Wiring uses the deviceset's own connects (first two pins), so 2-terminal parts get a real circuit and big ICs get a structurally-valid integration proof.", {"libraryPath": "required str (WINDOWS path .lbr; stage from container first)", "deviceset": "optional str (defaults to the library's first deviceset)"}, {"success": "bool", "schPath": "str", "brdPath": "str", "circuit": "str"}, 300, None, True, {"libraryPath": "C:/Temp/IN-S42ATR.lbr"}),+ ("fusion_screenshot_fusion", "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)", {"hwnd": "optional int (a dialog; default the main window)"}, {"success": "bool", "savedTo": "str", "sizeKB": "int"}, 60, None, False, {}), ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}), ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}), ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),
server.py+176−3@@ -2071,7 +2071,7 @@ ADDIN_COMMAND_MAP = { # timeout in http_server.py should be the source of truth; these are # matched to that so urllib doesn't cut off before the add-in does. ADDIN_COMMAND_TIMEOUTS = {- "show_symbol": 180, "show_footprint": 180, "show_3d_package": 300,+ "show_symbol": 180, "show_footprint": 180, "show_3d_package": 300, "make_test_project": 300, "demo_progress": 15, # getPhysicalProperties runs on Fusion's MAIN thread per component, so a big assembly # (or assembly_bom with includePhysicalProperties) is slow. Give both real headroom.@@ -5860,6 +5860,177 @@ def _glb_export_async_status(args: dict) -> dict: return out +++# ── fusion_make_test_project (John, 2026-08-15): the Web Control demo's PROJECT button.+# "None of our other adom user customers will even have a BQ25792" - a customer's session+# has exactly ONE thing: a component page's .lbr. This verb closes the circle from it:+# it GENERATES a minimal test circuit (the part + a generated series resistor on a small+# board) as real EAGLE .sch/.brd server-side, then imports the pair as a NEW electronics+# design. No cloud, no sign-in, no pre-existing content. The schematic/2D/3D buttons then+# work on the result.++def _mtp_extract(lbr_text, deviceset):+ """Pull the pieces the generator needs out of the .lbr."""+ def block_of(tag, name):+ m = re.search(r'<%s name="%s"[\s>].*?</%s>' % (tag, re.escape(name), tag), lbr_text, re.S)+ return m.group(0) if m else None+ ds = block_of("deviceset", deviceset)+ if not ds:+ return None, "deviceset %r not found in the library" % deviceset+ gate = re.search(r'<gate name="([^"]+)" symbol="([^"]+)"', ds)+ dev = re.search(r'<device name="[^"]*" package="([^"]+)"', ds)+ if not (gate and dev):+ return None, "deviceset %r has no gate/package device mapping" % deviceset+ connects = re.findall(r'<connect gate="[^"]+" pin="([^"]+)" pad="([^"]+)"', ds)+ sym = block_of("symbol", gate.group(2))+ pkg = block_of("package", dev.group(1))+ if not (sym and pkg and connects):+ return None, "library is missing the symbol/package/connects for %r" % deviceset+ layers = re.search(r'<layers>.*?</layers>', lbr_text, re.S)+ return {"deviceset": ds, "symbol": sym, "package": pkg, "connects": connects,+ "symName": gate.group(2), "gateName": gate.group(1), "pkgName": dev.group(1),+ "layers": layers.group(0) if layers else ""}, None+++_MTP_R_SYM = ('<symbol name="ADOMR"><wire x1="-2.54" y1="1.016" x2="2.54" y2="1.016" width="0.254" layer="94"/>'+ '<wire x1="2.54" y1="1.016" x2="2.54" y2="-1.016" width="0.254" layer="94"/>'+ '<wire x1="2.54" y1="-1.016" x2="-2.54" y2="-1.016" width="0.254" layer="94"/>'+ '<wire x1="-2.54" y1="-1.016" x2="-2.54" y2="1.016" width="0.254" layer="94"/>'+ '<pin name="1" x="-5.08" y="0" visible="off" length="short"/>'+ '<pin name="2" x="5.08" y="0" visible="off" length="short" rot="R180"/>'+ '<text x="-2.54" y="1.524" size="1.778" layer="95">>NAME</text>'+ '<text x="-2.54" y="-3.302" size="1.778" layer="96">>VALUE</text></symbol>')+_MTP_R_PKG = ('<package name="ADOMR0603"><description>Resistor 0603</description>'+ '<smd name="1" x="-0.85" y="0" dx="1" dy="1" layer="1"/>'+ '<smd name="2" x="0.85" y="0" dx="1" dy="1" layer="1"/>'+ '<wire x1="-0.4" y1="0.5" x2="0.4" y2="0.5" width="0.127" layer="21"/>'+ '<wire x1="-0.4" y1="-0.5" x2="0.4" y2="-0.5" width="0.127" layer="21"/>'+ '<text x="-1" y="0.8" size="0.8" layer="25">>NAME</text></package>')+_MTP_R_DS = ('<deviceset name="ADOMR" prefix="R"><description>generated test resistor</description>'+ '<gates><gate name="G$1" symbol="ADOMR" x="0" y="0"/></gates>'+ '<devices><device name="" package="ADOMR0603">'+ '<connects><connect gate="G$1" pin="1" pad="1"/><connect gate="G$1" pin="2" pad="2"/></connects>'+ '<technologies><technology name=""/></technologies></device></devices></deviceset>')+_MTP_SCH_LAYERS = ('<layers>'+ '<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes"/>'+ '<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes"/>'+ '<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes"/>'+ '<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>'+ '<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes"/></layers>')+_MTP_HEAD = ('<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE eagle SYSTEM "eagle.dtd">\n'+ '<eagle version="9.6.2">\n<drawing>\n<settings><setting alwaysvectorfont="no"/></settings>'+ '<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" '+ 'altdistance="0.01" altunitdist="inch" altunit="inch"/>')+++def _mtp_generate(lib_name, ex, part_name):+ """Build the .sch/.brd pair text. Wiring: R1.2 -> the part's FIRST connected pin;+ GND -> its second; VCC -> R1.1. Electrically meaningful for 2-terminal parts,+ structurally valid for anything (the test project proves design integration)."""+ pins = [p for p, _ in ex["connects"]]+ pads = {p: pad for p, pad in ex["connects"]}+ p1, p2 = pins[0], (pins[1] if len(pins) > 1 else pins[0])+ lib = ('<library name="%s"><packages>%s</packages><symbols>%s</symbols><devicesets>%s</devicesets></library>'+ % (lib_name, ex["package"], ex["symbol"], ex["deviceset"]))+ rlib = ('<library name="adomdemo"><packages>%s</packages><symbols>%s</symbols><devicesets>%s</devicesets></library>'+ % (_MTP_R_PKG, _MTP_R_SYM, _MTP_R_DS))+ g = ex["gateName"]+ sch = (_MTP_HEAD + _MTP_SCH_LAYERS ++ '<schematic xreflabel="%F%N/%S.%C%R" xrefpart="/%S.%C%R">'+ '<libraries>' + lib + rlib + '</libraries>'+ '<attributes/><variantdefs/><classes><class number="0" name="default" width="0" drill="0"/></classes>'+ '<parts>'+ '<part name="U1" library="' + lib_name + '" deviceset="' + part_name + '" device=""/>'+ '<part name="R1" library="adomdemo" deviceset="ADOMR" device="" value="330"/>'+ '</parts><sheets><sheet><plain>'+ '<text x="0" y="30.48" size="2.54" layer="97">Adom generated test circuit: ' + part_name + '</text>'+ '</plain><instances>'+ '<instance part="R1" gate="G$1" x="12.7" y="12.7"/>'+ '<instance part="U1" gate="' + g + '" x="38.1" y="12.7"/>'+ '</instances><busses/><nets>'+ '<net name="N1" class="0"><segment>'+ '<wire x1="17.78" y1="12.7" x2="25.4" y2="12.7" width="0.1524" layer="91"/>'+ '<pinref part="R1" gate="G$1" pin="2"/><pinref part="U1" gate="' + g + '" pin="' + p1 + '"/>'+ '</segment></net>'+ '<net name="VCC" class="0"><segment>'+ '<wire x1="7.62" y1="12.7" x2="2.54" y2="12.7" width="0.1524" layer="91"/>'+ '<pinref part="R1" gate="G$1" pin="1"/></segment></net>'+ '<net name="GND" class="0"><segment>'+ '<pinref part="U1" gate="' + g + '" pin="' + p2 + '"/></segment></net>'+ '</nets></sheet></sheets></schematic></drawing></eagle>\n')+ brd = (_MTP_HEAD + ex["layers"] ++ '<board><plain>'+ '<wire x1="0" y1="0" x2="14" y2="0" width="0.2" layer="20"/>'+ '<wire x1="14" y1="0" x2="14" y2="9" width="0.2" layer="20"/>'+ '<wire x1="14" y1="9" x2="0" y2="9" width="0.2" layer="20"/>'+ '<wire x1="0" y1="9" x2="0" y2="0" width="0.2" layer="20"/></plain>'+ '<libraries><library name="' + lib_name + '"><packages>' + ex["package"] + '</packages></library>'+ '<library name="adomdemo"><packages>' + _MTP_R_PKG + '</packages></library></libraries>'+ '<attributes/><variantdefs/><classes><class number="0" name="default" width="0" drill="0"/></classes>'+ '<designrules name="default"/><autorouter/>'+ '<elements>'+ '<element name="R1" library="adomdemo" package="ADOMR0603" value="330" x="3.5" y="4.5"/>'+ '<element name="U1" library="' + lib_name + '" package="' + ex["pkgName"] + '" value="" x="9.5" y="4.5"/>'+ '</elements><signals>'+ '<signal name="N1"><contactref element="R1" pad="2"/><contactref element="U1" pad="' + pads[p1] + '"/>'+ '<wire x1="4.35" y1="4.5" x2="8.5" y2="4.5" width="0.3" layer="1"/></signal>'+ '<signal name="VCC"><contactref element="R1" pad="1"/></signal>'+ '<signal name="GND"><contactref element="U1" pad="' + pads[p2] + '"/></signal>'+ '</signals></board></drawing></eagle>\n')+ return sch, brd+++def _orchestrate_make_test_project(args: dict) -> dict:+ """Generate + import a minimal test project around a library part.++ args: libraryPath (required, Windows path .lbr), deviceset (optional: defaults to+ the library's first deviceset).+ """+ import os+ import tempfile+ lbr_path = (args.get("libraryPath") or "").replace("\\", "/")+ if not lbr_path:+ return {"success": False, "error": "libraryPath (.lbr, Windows path) required.",+ "_hint": "Stage the component page's .lbr onto the box first (send_files)."}+ try:+ lbr_text = open(lbr_path, encoding="utf-8", errors="replace").read()+ except Exception as e:+ return {"success": False, "error": "could not read %s: %s" % (lbr_path, e)}+ deviceset = args.get("deviceset")+ if not deviceset:+ m = re.search(r'<deviceset name="([^"]+)"', lbr_text)+ if not m:+ return {"success": False, "error": "no deviceset found in the library"}+ deviceset = m.group(1)+ ex, err = _mtp_extract(lbr_text, deviceset)+ if err:+ return {"success": False, "error": err}+ lib_name = re.sub(r"[^A-Za-z0-9_-]", "_", os.path.basename(lbr_path).rsplit(".", 1)[0])+ sch_text, brd_text = _mtp_generate(lib_name, ex, deviceset)+ d = os.path.join(tempfile.gettempdir(), "adom-fusion-wrap")+ os.makedirs(d, exist_ok=True)+ base = "ADOM-TEST-" + re.sub(r"[^A-Za-z0-9_-]", "_", deviceset)[:40]+ sch_path = os.path.join(d, base + ".sch").replace("\\", "/")+ brd_path = os.path.join(d, base + ".brd").replace("\\", "/")+ open(sch_path, "w", encoding="utf-8").write(sch_text)+ open(brd_path, "w", encoding="utf-8").write(brd_text)+ res = _handle_new_electronics_from_eagle({"schPath": sch_path, "brdPath": brd_path})+ out = {"success": bool(res.get("success")), "schPath": sch_path, "brdPath": brd_path,+ "deviceset": deviceset, "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset}+ if not res.get("success"):+ out["error"] = "import failed: " + str(res.get("error"))[:200]+ out["_hint"] = str(res.get("_hint") or "")[:200]+ return out+ out["_hint"] = ("Test project imported as a NEW electronics design. Now the whole chain works: "+ "fusion_show_schematic -> fusion_show_2d_board -> fusion_show_3d_board. "+ "Generated from NOTHING but the component's .lbr - no cloud, no sign-in.")+ return out++ def _orchestrate_demo(args: dict) -> dict: """FIRST-TIME-USER DEMO: take a brand-new user from nothing to "wow" in one verb. @@ -6302,7 +6473,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "generate_package", "export_optimized_glb", "board_stackup", "save_to_cloud", "start",- "show_symbol", "show_footprint", "show_3d_package",+ "show_symbol", "show_footprint", "show_3d_package", "make_test_project", } if command in _WINDOW_RAISING: if (args or {}).get("foreground"):@@ -6323,7 +6494,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> # The user should launch Fusion themselves; we just report the status. # Commands that need Fusion running (add-in commands + orchestrated commands) _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}- if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb", "board_stackup", "show_symbol", "show_footprint", "show_3d_package") or command in _OPEN_WITH_SCREENSHOT:+ if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb", "board_stackup", "show_symbol", "show_footprint", "show_3d_package", "make_test_project") or command in _OPEN_WITH_SCREENSHOT: # Issue #326: Fusion may be installed under a different Windows user profile than the current # one, so file-path detection fails even though Fusion is running. Check if it's actually # running (via tasklist) as a reliable fallback. If running, it's installed somewhere.@@ -6433,6 +6604,8 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> return _merge_dialog_array(_orchestrate_show_footprint(args)) if command == "show_3d_package": return _merge_dialog_array(_orchestrate_show_3d_package(args))+ if command == "make_test_project":+ return _merge_dialog_array(_orchestrate_make_test_project(args)) if command == "save_lbr": return _merge_dialog_array(_orchestrate_save_lbr(args)) if command == "attach_3d_package":
tests/run_verb_tests.py+10@@ -228,6 +228,9 @@ t("fusion_new_electronics_from_eagle", "manual", note="drives native dialogs") # ── stage 6: doc-creating flows LAST (they change the active document) ────── t("fusion_import_step", "weekly", {"_dynamic": "staged_step"}, {"ok": True}, "creates a NEW design doc - must run after every active-doc-dependent test")+t("fusion_make_test_project", "weekly", {"_dynamic": "staged_lbr_mtp"}, {"ok": True, "fields": ["schPath", "brdPath"]},+ "from-scratch project generation around the staged sample part (John's no-BQ25792 rule)")+t("fusion_screenshot_fusion", "daily", {}, {"ok": True, "fields": ["savedTo"]}, "background main-window capture") t("fusion_demo#tour", "weekly", {"mode": "tour", "includeDesign": False, "_pollTour": 300}, {"ok": True}, "the six-content-type showcase (issue #29): async start + progress polls until done") t("fusion_demo_progress#unknown", "negative", {"demoId": "tour-nope"}, {"pred": "must_refuse"})@@ -325,6 +328,13 @@ def stage_dynamic(target, kind): write_file-in-argv blew Errno 7 on the 245KB STEP, 2026-08-14). """ try:+ if kind == "staged_lbr_mtp":+ fname = "ADS8588SIPM.fusion.lbr"+ src = str(REPO / "examples" / fname)+ r, _ = call(target, "send_files", {"filePaths": [src], "dest": SCRATCH}, timeout=180)+ if not (r.get("success") or r.get("status") == "ok"):+ return None, "send_files: " + str(r.get("error"))[:80]+ return {"libraryPath": SCRATCH + "/" + fname}, None if kind in ("staged_step", "staged_lbr"): fname = "ADS8588SIPM.step" if kind == "staged_step" else "ADS8588SIPM.fusion.lbr" src = str(REPO / "examples" / fname)