← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.60\ No newline at end of file+1.13.61\ No newline at end of file
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"   ]-}\ No newline at end of file+}
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)