← Commit history

v1.6.28: fusion_set_preference / fusion_get_preferences (theme/dark mode, scroll-invert, orbit scheme, orientation, units) - live, no restart

John Lauer ·9cce692703 ·3mo ago ·parent ce4d8ea
7 files changed +178−8
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.27\ No newline at end of file+1.6.28\ No newline at end of file
CHANGELOG.md+17
@@ -3,6 +3,23 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.28 - 2026-07-05++- **New `fusion_set_preference` + `fusion_get_preferences` verbs** - drive Fusion's appearance and+  navigation settings programmatically, applied LIVE (no restart). Server-only orchestrators (they+  compose an `adsk` preferences script and run it via the existing add-in `run_modeling_script`, so+  no add-in redeploy). Friendly keys: `theme` (light/darkblue/darkgray/classic/device/dark),+  `invertScrollZoom` (mouse scroll/zoom direction), `orbitScheme`+  (fusion360/alias/inventor/solidworks/tinkercad/powermill), `modelingOrientation` (yup/zup),+  `gestureNav`, `cameraPivot`, `lengthUnit` (mm/cm/m/in/ft, active design). Per-key outcome is+  reported in `applied` and the resulting state in `current`. Discovered via a live introspection of+  `app.preferences.generalPreferences` on ADOMBASELINE.+- **Note on theme availability:** not every Fusion build ships every theme. On the current webdeploy+  only **LightGray, DarkBlue, Device** were available (`Dark Gray theme not available` / `Classic+  theme not available` raise on assignment), so the verb tries candidates and reports per-key+  failures; `dark` resolves to Dark Gray then falls back to Dark Blue. Dark mode was set live on the+  VM's Fusion this way (Dark Blue).+ ## 1.6.27 - 2026-07-05  - **Fixed a false-positive in 1.6.26's new `needsSignin` readiness detection** (caught live before
SKILL.md+2
@@ -127,6 +127,8 @@ are dimmed). So: | `fusion_send_key` | `send_key` | Send a keyboard shortcut to Fusion | `hwnd`, `key` | | `fusion_addin_status` | `addin_status` | Report whether the in-process Adom add-in is loaded + healthy | — | | `fusion_generate_package` | `generate_package` | Generate an IPC package via Fusion's built-in EPG (~50 families) + raised-WHITE marking (longest-axis, measured 10% margin, multi-line MPN+variant) + colored STEP + sidecar manifest - in ONE call | `type`, `params` (mm), `etch`, `etchStyle`, `outputStep` |+| `fusion_set_preference` | `set_preference` | Set appearance/navigation prefs LIVE (no restart): `theme` (light/darkblue/darkgray/classic/device/dark), `invertScrollZoom`, `orbitScheme` (fusion360/alias/inventor/solidworks/tinkercad/powermill), `modelingOrientation` (yup/zup), `gestureNav`, `cameraPivot`, `lengthUnit` (mm/cm/m/in/ft). Per-key result in `applied`; some themes aren't in every build (falls back) | any of the keys |+| `fusion_get_preferences` | `get_preferences` | Read current appearance/navigation prefs as the friendly values `set_preference` accepts | — |  ## Extended commands via the in-process add-in 
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.27",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.27/adom-bridge-fusion-v1.6.27.zip",-  "sha256": "73e9591b4d902770827920f1d059a232609945401b2524de6bf8d842137ad4c9",-  "size": 323919,+  "version": "1.6.28",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.28/adom-bridge-fusion-v1.6.28.zip",+  "sha256": "099cd9e3c87ae9f0b638eab24cfd3807f173c485c3c46578baa669eec3fecb86",+  "size": 327488,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-05T12:24:43Z"+  "released_at": "2026-07-05T12:39:42Z" }
bridge.json+6−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.27",+  "version": "1.6.28",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",@@ -59,6 +59,7 @@     "fusion_export_usdz",     "fusion_generate_package",     "fusion_get_app_state",+    "fusion_get_preferences",     "fusion_install_fusion",     "fusion_kill",     "fusion_load_design_rules",@@ -69,6 +70,7 @@     "fusion_run_modeling_script",     "fusion_search_cloud_files",     "fusion_set_design_rules",+    "fusion_set_preference",     "fusion_show_2d_board",     "fusion_show_3d_board",     "fusion_show_schematic",@@ -168,7 +170,9 @@       "build_library_3d": 1800,       "capture_library_views": 600,       "start": 90,-      "install_fusion": 90+      "install_fusion": 90,+      "set_preference": 40,+      "get_preferences": 25     },     "prefixes": [       {
describe.py+7
@@ -100,6 +100,13 @@ _T = [      {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"},      {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}), +    ("fusion_set_preference", "Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart. Keys: theme (light/darkblue/darkgray/classic/device/dark; some builds only ship LightGray/DarkBlue/Device, failures reported per-key), invertScrollZoom (bool - mouse scroll/zoom direction), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - active design). Per-key outcome in `applied`, resulting state in `current`.",+     {"theme": "optional str", "invertScrollZoom": "optional bool", "orbitScheme": "optional str", "modelingOrientation": "optional str", "gestureNav": "optional bool", "cameraPivot": "optional bool", "lengthUnit": "optional str"},+     {"applied": "obj", "current": "obj"}, 30, None, True, {"theme": "dark"}),++    ("fusion_get_preferences", "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",+     {}, {"current": "obj"}, 20, None, False, {}),+     # ── CAD & 3D export ────────────────────────────────────────────────────     ("fusion_export_step", "Export the active design to STEP (needs the 3D view).",      {"outputPath": "required str"}, {"format": "str", "fileSizeKB": "int"}, 300, None, True, {"outputPath": "C:/out/board.step"}),
server.py+140
@@ -837,9 +837,149 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     return result  +# ── Fusion preferences (theme / navigation / units) ──────────────────────────+# Set via the adsk preferences API inside the live session (run_modeling_script),+# so this is a SERVER-only orchestrator - no add-in redeploy. Friendly keys map to+# adsk enums; unknown/failed keys are reported per-key (some themes, e.g. Dark Gray+# / Classic, aren't shipped in every Fusion build - proven live 2026-07-05, only+# LightGray/DarkBlue/Device were available). Theme changes apply LIVE (no restart).+_PREF_SCRIPT = r'''+import json+gp = app.preferences.generalPreferences+T  = adsk.core.UserInterfaceThemes+PZ = adsk.core.PanZoomOrbitShortcuts+MO = adsk.core.DefaultModelingOrientations+DU = adsk.fusion.DistanceUnits++THEME = {'light':[T.LightGrayUserInterfaceTheme],'lightgray':[T.LightGrayUserInterfaceTheme],+         'dark':[T.DarkGrayUserInterfaceTheme,T.DarkBlueUserInterfaceTheme],+         'darkgray':[T.DarkGrayUserInterfaceTheme],'darkblue':[T.DarkBlueUserInterfaceTheme],+         'classic':[T.ClassicUserInterfaceTheme],'device':[T.DeviceUserInterfaceTheme],+         'auto':[T.DeviceUserInterfaceTheme]}+THEME_NAME  = {0:'classic',1:'lightgray',2:'darkblue',3:'darkgray',4:'device'}+ORBIT = {'fusion360':PZ.Fusion360PanZoomOrbitShortcut,'fusion':PZ.Fusion360PanZoomOrbitShortcut,+         'alias':PZ.AliasPanZoomOrbitShortcut,'inventor':PZ.InventorPanZoomOrbitShortcut,+         'solidworks':PZ.SolidWorksPanZoomOrbitShortcut,'tinkercad':PZ.TinkercadPanZoomOrbitShortcut,+         'powermill':PZ.PowerMillPanZoomOrbitShortcut}+ORBIT_NAME  = {0:'fusion360',1:'alias',2:'inventor',3:'solidworks',4:'tinkercad',5:'powermill'}+ORIENT = {'yup':MO.YUpModelingOrientation,'zup':MO.ZUpModelingOrientation}+ORIENT_NAME = {0:'yup',1:'zup'}+UNITS = {'mm':DU.MillimeterDistanceUnits,'cm':DU.CentimeterDistanceUnits,'m':DU.MeterDistanceUnits,+         'in':DU.InchDistanceUnits,'inch':DU.InchDistanceUnits,'ft':DU.FootDistanceUnits}+UNITS_NAME  = {0:'mm',1:'cm',2:'m',3:'in',4:'ft'}++def read_current():+    cur = {+      'theme': THEME_NAME.get(gp.userInterfaceTheme, gp.userInterfaceTheme),+      'activeTheme': THEME_NAME.get(gp.activeUserInterfaceTheme, gp.activeUserInterfaceTheme),+      'invertScrollZoom': bool(gp.isZoomDirectionReversed),+      'orbitScheme': ORBIT_NAME.get(gp.panZoomOrbitShortcuts, gp.panZoomOrbitShortcuts),+      'modelingOrientation': ORIENT_NAME.get(gp.defaultModelingOrientation, gp.defaultModelingOrientation),+      'gestureNav': bool(gp.isGestureBasedViewNavigationUsed),+      'cameraPivot': bool(gp.isCameraPivotEnabled),+    }+    try:+        cur['lengthUnit'] = UNITS_NAME.get(design.fusionUnitsManager.distanceDisplayUnits,+                                           design.fusionUnitsManager.distanceDisplayUnits)+    except Exception:+        pass+    return cur++def try_set(obj, attr, cands, want):+    err = None+    for c in cands:+        if c is None: continue+        try:+            setattr(obj, attr, c)+            return {'requested': want, 'ok': True}+        except Exception as e:+            err = str(e)+    return {'requested': want, 'ok': False, 'error': err or 'no valid value'}++reqs = json.loads(__REQS__)+applied = {}+for k, v in reqs.items():+    kv = v if isinstance(v, bool) else str(v).strip().lower()+    if k == 'theme':+        c = THEME.get(kv);  applied[k] = try_set(gp,'userInterfaceTheme', c or [], v) if c else {'requested':v,'ok':False,'error':'unknown theme (use light/darkblue/darkgray/classic/device/dark)'}+    elif k in ('invertScrollZoom','reverseZoom'):+        applied['invertScrollZoom'] = try_set(gp,'isZoomDirectionReversed',[bool(v)],v)+    elif k == 'orbitScheme':+        c = ORBIT.get(kv);  applied[k] = try_set(gp,'panZoomOrbitShortcuts',[c],v) if c is not None else {'requested':v,'ok':False,'error':'unknown orbitScheme'}+    elif k == 'modelingOrientation':+        c = ORIENT.get(kv); applied[k] = try_set(gp,'defaultModelingOrientation',[c],v) if c is not None else {'requested':v,'ok':False,'error':'unknown modelingOrientation (yup/zup)'}+    elif k == 'gestureNav':+        applied[k] = try_set(gp,'isGestureBasedViewNavigationUsed',[bool(v)],v)+    elif k == 'cameraPivot':+        applied[k] = try_set(gp,'isCameraPivotEnabled',[bool(v)],v)+    elif k == 'lengthUnit':+        c = UNITS.get(kv)+        if c is None: applied[k] = {'requested':v,'ok':False,'error':'unknown unit (mm/cm/m/in/ft)'}+        else:+            try: applied[k] = try_set(design.fusionUnitsManager,'distanceDisplayUnits',[c],v)+            except Exception as e: applied[k] = {'requested':v,'ok':False,'error':str(e)}+    else:+        applied[k] = {'requested': v, 'ok': False, 'error': 'unknown preference key'}++result = {'applied': applied, 'current': read_current()}+print(json.dumps(result))+'''+++def _shape_pref_result(res: dict, wrote: bool) -> dict:+    if not res or not res.get("success"):+        return {"success": False,+                "error": (res or {}).get("error", "preferences script did not run"),+                "_hint": "fusion_set_preference/get_preferences need Fusion running + signed in "+                         "(fusion_readiness -> ready:true). If needsSignin, drive the sign-in first."}+    data = (res.get("data") or {}).get("result") or {}+    applied = data.get("applied", {}) or {}+    current = data.get("current", {}) or {}+    out = {"success": True, "applied": applied, "current": current}+    if wrote:+        oks = [k for k, v in applied.items() if v.get("ok")]+        fails = {k: v.get("error") for k, v in applied.items() if not v.get("ok")}+        if fails:+            out["partial"] = True+            out["_hint"] = ("Applied live: %s. FAILED: %s. Note: some themes (Dark Gray/Classic) "+                            "aren't shipped in every Fusion build - use 'darkblue' or 'device'. See "+                            "`current` for the resulting state." %+                            (", ".join(oks) or "(none)",+                             "; ".join("%s=%s" % (k, e) for k, e in fails.items())))+        else:+            out["_hint"] = ("Applied LIVE (no restart needed): %s. See `current` for resulting state."+                            % ", ".join(oks))+    else:+        out["_hint"] = ("Current Fusion preferences. Change any with fusion_set_preference, keys: "+                        "theme (light/darkblue/darkgray/classic/device/dark), invertScrollZoom (bool), "+                        "orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), "+                        "modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), "+                        "lengthUnit (mm/cm/m/in/ft - applies to the active design).")+    return out+++def _handle_set_preference(fusion_info: dict, args: dict) -> dict:+    reqs = {k: v for k, v in (args or {}).items() if k not in ("settle",)}+    if not reqs:+        return {"success": False, "error": "No preferences given.",+                "_hint": "Pass e.g. {\"theme\":\"dark\"} or {\"invertScrollZoom\":true,"+                         "\"orbitScheme\":\"solidworks\"}. Call fusion_get_preferences to see current values + keys."}+    script = _PREF_SCRIPT.replace("__REQS__", json.dumps(json.dumps(reqs)))+    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=30)+    return _shape_pref_result(res, wrote=True)+++def _handle_get_preferences(fusion_info: dict, args: dict) -> dict:+    script = _PREF_SCRIPT.replace("__REQS__", json.dumps(json.dumps({})))+    res = _proxy_to_addin("run_modeling_script", {"script": script}, timeout=20)+    return _shape_pref_result(res, wrote=False)++ COMMAND_HANDLERS = {     "describe": lambda fi, args: describe.handle_describe(args),     "readiness": _handle_fusion_readiness,+    "set_preference": _handle_set_preference,+    "get_preferences": _handle_get_preferences,     "install_fusion": _handle_install_fusion,     "open_design": handle_open_design,     "close": handle_close_fusion,  # deprecated alias - use stop (graceful) / kill (force)