← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.72\ No newline at end of file+1.13.73\ No newline at end of file
CHANGELOG.md+1−1
@@ -3,7 +3,7 @@ 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` -## Next release+## 1.13.73 (2026-11-05)  `fusion_move_parts` keeps a part's side (adom-aiflow#142, Andy Engevold). A rotation given as a number took the mirror off, so a bottom part moved to the top. A number now keeps the side. To change it, give `side` (top or bottom) or `flip: true`, the standard's arguments, or a string that names it (`R90`, `MR90`); parts that change side are listed in `sideChanged`. If any part doesn't land, every move is undone (`undone`), where the parts already moved used to stay moved. 
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.72",+  "version": "1.13.73",   "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.",
contracts/describe.json+2−2
@@ -1,6 +1,6 @@ {   "contract": "fusion-bridge verb catalog",-  "frozen": "2026-11-04",+  "frozen": "2026-11-05",   "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.",   "source": "describe.py",   "verbs": [@@ -19910,7 +19910,7 @@         "verified": "bool"       },       "statusVerb": "fusion_routing_state",-      "summary": "Place parts on the live board: one MOVE (absolute, mm) and ROTATE per part, each its own undo step, then a fresh export reads every element back. The layout half of the AI flow's Fusion lane (adom-aiflow place land). Coordinates are Fusion's (y up).",+      "summary": "Place parts on the live board: one MOVE (absolute, mm) and ROTATE per part, each its own undo step, then a fresh export reads every element back; if any part didn't land, every move is undone. A rotation given as a number keeps the part's side, and is EAGLE's angle for that side. To change side, give side (top or bottom) or flip:true, or a string that names it (R90 top, MR90 bottom); parts that changed side are listed in sideChanged, and their tracks no longer reach their pads until they're routed again. The layout half of the AI flow's Fusion lane (adom-aiflow place land). Coordinates are Fusion's (y up).",       "timeoutSeconds": 120     },     {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.72"+version = "1.13.73" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.72"+version = "1.13.73" 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.72"+DASH_VERSION="1.13.73" 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.72",+  "version": "1.13.73",   "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.72",+  "version": "1.13.73",   "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"   ]-}\ No newline at end of file+}
rust/Cargo.lock+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.72"+version = "1.13.73" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.72"+version = "1.13.73" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.72"+version = "1.13.73" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.72"+version = "1.13.73" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.72"+version = "1.13.73" 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.72"+version = "1.13.73" edition = "2021" license = "MIT" 
tests/glenn_drill.py+22
@@ -101,6 +101,28 @@ if want('138'):     check('remove by id takes only that one', r.get('removed') == 1 and [p.get('net') for p in ps] == ['NC'], (r.get('errorCode'), r.get('error'), [p.get('net') for p in ps]))     s = st(doc); r = call('fusion_remove_pours', {'expectDocument': doc, 'boardId': s.get('boardId'), 'expectedRevision': s.get('revision'), 'pours': [{'id': 'pour:x:1:nope'}]}); check('an unknown id is refused', r.get('errorCode') == 'unknown_pour', r.get('errorCode')); nodialog('after the pours'); closeall() +if want('move'):+    print('--- adom-aiflow#142 a move keeps the side')+    for linked in (False, True):+        if linked:+            seed('GMoveL'); run = call('fusion_new_electronics_from_eagle', {'schPath': D + '/GMoveL.sch', 'brdPath': D + '/GMoveL.brd'}).get('runId')+            for _ in range(40):+                time.sleep(4); x = call('fusion_eagle_import_status', {'runId': run})+                if x.get('stage') in ('done', 'failed'): break+            call('fusion_show_2d_board', {}); time.sleep(2); doc = active(); tag = 'linked board: '+        else:+            doc = board('GMove'); tag = 'no schematic: '+        rot = lambda ref: next(e['rot'] for e in st(doc).get('elements') or [] if e['name'] == ref).lstrip('S')+        def mv(**p):+            s = st(doc); return call('fusion_move_parts', {'expectDocument': doc, 'boardId': s.get('boardId'), 'expectedRevision': s.get('revision'), 'parts': [dict({'ref': 'R1', 'x': 12, 'y': 20}, **p)]})+        r = mv(side='bottom'); check(tag + 'side bottom flips it', r.get('success') and r.get('sideChanged') == ['R1'] and rot('R1').startswith('MR'), (r.get('errorCode'), r.get('error'), r.get('mismatched')))+        r = mv(x=14, rotation=90); check(tag + 'a number keeps it on the bottom', r.get('success') and r.get('sideChanged') == [] and rot('R1') == 'MR90', (r.get('errorCode'), r.get('mismatched'), rot('R1')))+        r = mv(x=15); check(tag + 'no rotation keeps side and angle', r.get('success') and rot('R1') == 'MR90', (r.get('errorCode'), rot('R1')))+        r = mv(rotation='MR180'); check(tag + 'an MR string still works', r.get('success') and r.get('sideChanged') == [] and rot('R1') == 'MR180', (r.get('errorCode'), rot('R1')))+        r = mv(flip=True); check(tag + 'flip turns it back', r.get('success') and r.get('sideChanged') == ['R1'] and not rot('R1').startswith('M'), (r.get('errorCode'), r.get('mismatched'), rot('R1')))+        r = mv(rotation=270); check(tag + 'a number keeps a top part on top', r.get('success') and rot('R1') == 'R270', (r.get('errorCode'), rot('R1')))+        nodialog(tag + 'after the moves'); closeall()+ if want('145'):     print('--- #145 fusion_change_vias')     doc = board('GVia'); s = st(doc)