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 13 file(s)
13 files changed
+54−28
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.62+1.13.63
CHANGELOG.md+6@@ -3,6 +3,12 @@ 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.13.63 (2026-11-03)++`fusion_schematic_run` has a 20 minute budget in Adom Bridge. It had the 60 second default, so a long list reported `timed_out` while Fusion carried on working (#137).++`fusion_set_layer_count` repairs a board an earlier version converted: Fusion drops the bad via definition on load, so what such a board holds is a stack with no through via. Called again at the same count, it writes the stack again.+ ## 1.13.62 (2026-11-03) `fusion_schematic_run` takes a list of any length (#137, Andy Engevold): it was refused past 400 commands, about 57 parts at 7 commands each. The bridge
bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.62",+ "version": "1.13.63", "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.",@@ -502,6 +502,7 @@ "aps_search": 120, "aps_recent": 120, "autoroute": 300,+ "schematic_run": 1200, "set_track_width": 180, "remove_items": 150, "set_outline": 150,
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.13.62"+version = "1.13.63" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.62"+version = "1.13.63" edition = "2021" [dependencies]
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.13.62"+DASH_VERSION="1.13.63" 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.13.62",+ "version": "1.13.63", "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+2−2@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.13.62",+ "version": "1.13.63", "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": {@@ -152,4 +152,4 @@ "confirmed_user_skills": [ "fusion-design-efficiency" ]-}+}
rust/Cargo.lock+5−5@@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "adom-fusion-bridge"-version = "1.13.62"+version = "1.13.63" dependencies = [ "adom-eda-checks", "base64",@@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "fusion-core"-version = "1.13.62"+version = "1.13.63" dependencies = [ "serde", "serde_json",@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "fusion-lbr"-version = "1.13.62"+version = "1.13.63" dependencies = [ "regex", "roxmltree",@@ -322,14 +322,14 @@ dependencies = [ [[package]] name = "fusion-os-mac"-version = "1.13.62"+version = "1.13.63" dependencies = [ "fusion-core", ] [[package]] name = "fusion-os-win"-version = "1.13.62"+version = "1.13.63" dependencies = [ "fusion-core", "windows-sys 0.59.0",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"] # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.62"+version = "1.13.63" edition = "2021" license = "MIT"
rust/bridge/src/routing_script.py+8−2@@ -3108,8 +3108,14 @@ def stackup_for(count, total_mm, copper_mm, template): def stack_has_broken_viadef(board):- """True for a stack written by fusion_set_layer_count before 1.13.61: a via definition with no start or stop layer."""- return any(not (v.get('startlayer') and v.get('stoplayer')) for s_ in board.findall('.//layerstackup') for v in s_.findall('viadef'))+ """True for a stack written by fusion_set_layer_count before 1.13.61. Its via definition had no start or stop+ layer, and Fusion drops such a definition on load, so what is left is a stack with no through via at all+ (live 2026-11-03). A via with a layer range lands nothing on it."""+ for s_ in board.findall('.//layerstackup'):+ defs = s_.findall('viadef')+ if not defs or any(not (v.get('startlayer') and v.get('stoplayer')) for v in defs):+ return True+ return False def set_layer_count(app, root, board, meta, args):
tests/layer_drill.py+23−12@@ -54,21 +54,32 @@ check("edit_pours moves a pour's net", r.get('success') and any(p.get('net') == call('fusion_close_document', {'name': doc}) print('--- a board with the stack the old conversion wrote'); doc = seed('LDrillOld')-d = tempfile.gettempdir()+r = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('convert it first', r.get('success'), r.get('error')); time.sleep(2)+READ = """+import tempfile, os, xml.etree.ElementTree as ET+p = os.path.join(tempfile.gettempdir(), 'ldrill2.edru').replace(chr(92), '/'); app.executeTextCommand("Electron.run DRC SAVE '%s';" % p)+r = ET.parse(p).getroot(); r = r.find('designrules') if r.tag != 'designrules' else r+result = {'viadefs': [dict(v.attrib) for v in r.iter('viadef')], 'setup': [q.get('value') for q in r.findall('param') if q.get('name') == 'layerSetup']}+""" r = call('fusion_run_modeling_script', {'script': """ import tempfile, os, xml.etree.ElementTree as ET-p = os.path.join(tempfile.gettempdir(), 'ldrill.edru'); app.executeTextCommand("Electron.run DRC SAVE '%s';" % p.replace(chr(92), '/'))+p = os.path.join(tempfile.gettempdir(), 'ldrill.edru').replace(chr(92), '/'); app.executeTextCommand("Electron.run DRC SAVE '%s';" % p) t = ET.parse(p); r = t.getroot(); r = r.find('designrules') if r.tag != 'designrules' else r-for q in r.findall('param'):- if q.get('name') == 'layerSetup': q.set('value', '(1+2*15+16)')-st = r.find('layerstackup'); vs = st.find('viadef'); vs.attrib.clear(); vs.set('name', 'Thru'); vs.set('layers', '1:16'); t.write(p, encoding='utf-8', xml_declaration=True)-app.executeTextCommand("Electron.run SET CONFIRM NO; DRC LOAD '%s'; SET CONFIRM OFF;" % p.replace(chr(92), '/')); result = 'ok'-"""}); print(' old stack loaded:', r.get('success'), (r.get('error') or '')[:120]); time.sleep(2); dialogs()-b = len(st(doc).get('tracks') or [])-r, s0 = route(doc, 25); print(' route on the old stack:', r.get('success'), r.get('errorCode'), 'undone', r.get('undone'), 'viasNotLanded', r.get('viasNotLanded'))-if not r.get('success'):- check('a route that does not land comes off', r.get('undone') and len(st(doc).get('tracks') or []) == b, {k: r.get(k) for k in ('errorCode', 'undone', 'error')})-r = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('set_layer_count repairs it', r.get('success') and r.get('repairedViaDefinition'), {k: r.get(k) for k in ('errorCode', 'error', 'note', 'mutated')}); time.sleep(2)+v = r.find('layerstackup').find('viadef'); v.attrib.clear(); v.set('name', 'Thru'); v.set('layers', '1:16'); t.write(p, encoding='utf-8', xml_declaration=True)+app.executeTextCommand("Electron.run SET CONFIRM NO; DRC LOAD '%s'; SET CONFIRM OFF;" % p); result = 'ok'+"""}); print(' old stack loaded:', r.get('success'), (r.get('error') or '')[:150]); time.sleep(2); dialogs()+print(' Fusion now holds:', (call('fusion_run_modeling_script', {'script': READ}).get('data') or {}).get('result'))+b0 = len(st(doc).get('vias') or []); call('fusion_execute_text_command', {'command': "Electron.run CHANGE DRILL 0.4mm; VIA 'MID' 0.8mm ROUND 1-16 (6mm 6mm);"})+broken = len(st(doc).get('vias') or []) == b0; print(' the bug reproduces (a via 1-16 lands nothing):', broken)+b = len(st(doc).get('tracks') or []); v0 = len(st(doc).get('vias') or [])+s = st(doc); r = call('fusion_route_net', {'expectDocument': doc, 'boardId': s.get('boardId'), 'expectedRevision': s.get('revision'), 'net': 'NB', 'layer': 'F.Cu', 'width': 0.3, 'viaDiameter': 0.8, 'viaDrill': 0.4, 'points': [[2, 25], [4, 25], {'x': 6, 'y': 25, 'layer': 'B.Cu'}, [8, 25]]})+print(' route on the old stack:', r.get('success'), r.get('errorCode'), 'undone', r.get('undone'), 'viasNotLanded', r.get('viasNotLanded'), (r.get('error') or '')[:130])+if broken:+ check('a route that does not land comes off', r.get('errorCode') == 'readback_mismatch' and r.get('undone') and len(st(doc).get('tracks') or []) == b, {k: r.get(k) for k in ('errorCode', 'undone', 'error')})+elif r.get('success'):+ call('fusion_execute_text_command', {'command': 'Electron.run UNDO;'})+r = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('set_layer_count repairs it', r.get('success') and r.get('repairedViaDefinition') == True or not broken, {k: r.get(k) for k in ('errorCode', 'error', 'note', 'mutated', 'repairedViaDefinition')}); time.sleep(2)+print(' Fusion now holds:', (call('fusion_run_modeling_script', {'script': READ}).get('data') or {}).get('result')) via_lands(doc, "CHANGE DRILL 0.3mm; VIA 'MID' 0.6mm ROUND 1-16 (10mm 5mm);", 'via 1-16 lands after the repair') r2 = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('a second call has nothing to repair', r2.get('success') and not r2.get('mutated'), r2.get('mutated')) dialogs(); call('fusion_close_document', {'name': doc})
tests/test_routing.py+2@@ -699,6 +699,8 @@ class LayerCountTests(unittest.TestCase): self.assertFalse(r.stack_has_broken_viadef(b)) v = st.find('viadef'); v.attrib.clear(); v.set('name', 'Thru'); v.set('layers', '1:16') self.assertTrue(r.stack_has_broken_viadef(b))+ st.remove(v) # what Fusion actually holds after loading that: the definition dropped+ self.assertTrue(r.stack_has_broken_viadef(b))