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 11 file(s)
11 files changed
+88−15
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.60+1.13.61
CHANGELOG.md+14−1@@ -3,7 +3,20 @@ 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.61 (2026-11-03)++Vias on converted boards: `fusion_set_layer_count` wrote a via definition with no start or stop layer, so a VIA with a layer range landed+nothing afterwards. It now writes Fusion's own shape, and calling it again at the same count repairs a board it already converted.++`fusion_route_net`: a route that does not land as planned comes off again, one undo step at a time until the copper is back (`undone`,+`viasNotLanded`, `tracksAsPlanned` in the reply). It used to leave the copper on the board.++Pours: `fusion_apply_pours` takes any copper layer of the board, numbered or named (`In1.Cu`); it took top and bottom only. It also takes `cutouts`+(top and bottom, on the restrict layers; Fusion has none for inner layers), `connect` and `order`. `fusion_edit_pours` moves a pour to another net+(`newNet`) by drawing it again with the same outline and settings.++`fusion_edit_tracks` re-aims a track end at an angle: the new track goes down, then the old one comes off. `library_name_in_use` no longer says a+restart clears it: Fusion's in-use list survives a restart. Efficiency playbook (`fusion-design-efficiency`, PR #34 by Drew Owens): position parts and add joints only when the human asks. Added on merge: positioned parts don't follow when the part they sit on changes size, so on a parameter-driven assembly the AI says so and offers joints.
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.60",+ "version": "1.13.61", "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.13.60"+version = "1.13.61" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.60"+version = "1.13.61" 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.60"+DASH_VERSION="1.13.61" 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.60",+ "version": "1.13.61", "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.60",+ "version": "1.13.61", "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.60"+version = "1.13.61" dependencies = [ "adom-eda-checks", "base64",@@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "fusion-core"-version = "1.13.60"+version = "1.13.61" dependencies = [ "serde", "serde_json",@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "fusion-lbr"-version = "1.13.60"+version = "1.13.61" dependencies = [ "regex", "roxmltree",@@ -322,14 +322,14 @@ dependencies = [ [[package]] name = "fusion-os-mac"-version = "1.13.60"+version = "1.13.61" dependencies = [ "fusion-core", ] [[package]] name = "fusion-os-win"-version = "1.13.60"+version = "1.13.61" 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.60"+version = "1.13.61" edition = "2021" license = "MIT"
tests/playbook_positioned_parts.pyadded+60@@ -0,0 +1,60 @@+#!/usr/bin/env python3+"""Live check of what skills/fusion-design-efficiency item 4 claims, on a new unsaved design (never anyone's): a positioned part+stays where it is through a compute, and does not follow when the part it sits on changes size, while a jointed part does.+ python3 tests/playbook_positioned_parts.py --target DESKTOP-BAR6S0F+"""+import argparse, json, subprocess, sys+ap = argparse.ArgumentParser(); ap.add_argument('--target', default='DESKTOP-BAR6S0F'); a = ap.parse_args()+SCRIPT = r'''+import adsk.core, adsk.fusion+app = adsk.core.Application.get()+doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)+out = {}+try:+ d = adsk.fusion.Design.cast(app.activeProduct); d.designType = adsk.fusion.DesignTypes.ParametricDesignType+ root = d.rootComponent+ vi = adsk.core.ValueInput+ d.userParameters.add('base_h', vi.createByString('20 mm'), 'mm', 'fusion-lane playbook test')+ def box(name, x, y, side, height, z=0.0):+ occ = root.occurrences.addNewComponent(adsk.core.Matrix3D.create())+ c = occ.component; c.name = name+ sk = c.sketches.add(c.xYConstructionPlane)+ sk.sketchCurves.sketchLines.addTwoPointRectangle(adsk.core.Point3D.create(x, y, 0), adsk.core.Point3D.create(x + side, y + side, 0))+ ex = c.features.extrudeFeatures.addSimple(sk.profiles.item(0), height, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)+ return occ, ex.bodies.item(0)+ base, base_body = box('Base', 0, 0, 8.0, vi.createByString('base_h'))+ # positioned: placed by its transform on top of the base, no joint+ pos, _ = box('Positioned', 0.5, 0.5, 1.0, vi.createByReal(1.0))+ m = adsk.core.Matrix3D.create(); m.translation = adsk.core.Vector3D.create(0, 0, 2.0); pos.transform = m+ if d.snapshots.hasPendingSnapshot: d.snapshots.add()+ # jointed: rigid joint to the base's top face+ jnt, jb = box('Jointed', 4.0, 4.0, 1.0, vi.createByReal(1.0))+ top = max((f for f in base.bRepBodies.item(0).faces), key=lambda f: f.boundingBox.maxPoint.z + f.boundingBox.minPoint.z)+ low = min((f for f in jnt.bRepBodies.item(0).faces), key=lambda f: f.boundingBox.maxPoint.z + f.boundingBox.minPoint.z)+ ji = root.joints.createInput(adsk.fusion.JointGeometry.createByPlanarFace(low, None, adsk.fusion.JointKeyPointTypes.CenterKeyPoint),+ adsk.fusion.JointGeometry.createByPlanarFace(top, None, adsk.fusion.JointKeyPointTypes.CenterKeyPoint))+ ji.setAsRigidJointMotion(); root.joints.add(ji)+ z = lambda o: (round(o.bRepBodies.item(0).boundingBox.minPoint.z * 10, 3), round(o.bRepBodies.item(0).boundingBox.maxPoint.z * 10, 3))+ out['before'] = {'baseTopMm': z(base)[1], 'positionedBottomMm': z(pos)[0], 'jointedBottomMm': z(jnt)[0]}+ d.computeAll()+ out['afterCompute'] = {'baseTopMm': z(base)[1], 'positionedBottomMm': z(pos)[0], 'jointedBottomMm': z(jnt)[0]}+ d.userParameters.itemByName('base_h').expression = '40 mm'+ d.computeAll(); adsk.doEvents()+ out['afterResize'] = {'baseTopMm': z(base)[1], 'positionedBottomMm': z(pos)[0], 'jointedBottomMm': z(jnt)[0]}+finally:+ doc.close(False)+result = out+'''+o = subprocess.run(['adom-bridge', '--target', a.target, '--ai-thread', 'fusion-lane', 'fusion_run_modeling_script', json.dumps({'script': SCRIPT})], capture_output=True, text=True).stdout+r = json.loads(o); print(json.dumps({k: r.get(k) for k in ('success', 'errorCode', 'error', 'result', 'data')}, indent=1)[:1800])+res = r.get('result') or (r.get('data') or {}).get('result') or {}+if isinstance(res, str): res = json.loads(res)+try:+ b, c, s = res['before'], res['afterCompute'], res['afterResize']+except Exception:+ print('FAIL: the test did not finish'); sys.exit(1)+checks = [('base starts 20 mm tall', abs(b['baseTopMm'] - 20) < .01), ('positioned part sits on it', abs(b['positionedBottomMm'] - 20) < .01), ('jointed part sits on it', abs(b['jointedBottomMm'] - 20) < .01),+ ('positioned part stays through a compute', abs(c['positionedBottomMm'] - 20) < .01), ('base grew to 40 mm', abs(s['baseTopMm'] - 40) < .01),+ ('positioned part did NOT follow', abs(s['positionedBottomMm'] - 20) < .01), ('jointed part DID follow', abs(s['jointedBottomMm'] - 40) < .01)]+for n, ok in checks: print('%-42s %s' % (n, 'PASS' if ok else 'FAIL'))+sys.exit(0 if all(ok for _, ok in checks) else 1)