app
Adom Desktop - Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Desktop: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
#!/usr/bin/env python3
"""Install the AdomBridge add-in into EVERY Fusion add-in location.
Fusion has MOVED its per-user add-in directory across versions, and an add-in
in the wrong one is silently ignored (Fusion loads fine, the add-in never runs,
port 8774 stays down - issue #63, root-caused live on a fresh 2026 install):
NEW (2025+): %APPDATA%/Autodesk/FusionAddins/AdomBridge/ <- current Fusion scans THIS
legacy: %APPDATA%/Autodesk/Autodesk Fusion/API/AddIns/ <- older builds
legacy: %APPDATA%/Autodesk/Autodesk Fusion 360/API/AddIns/ <- oldest naming
Verified live (ADOMBASELINE, Fusion 2026.x): the add-in sat in the legacy
API/AddIns dir through a full launch and NEVER loaded; moved to FusionAddins it
came up in 20s with zero manual enabling (runOnStartup honored). So we install
to ALL locations - each Fusion version loads from the one it scans and ignores
the others (a duplicate instance would fail its 8774 bind and no-op). NEVER
assume a single path; scan all of TARGET_CANDIDATES.
Usage:
python install_addin.py
python install_addin.py --uninstall
"""
import shutil
import sys
from pathlib import Path
# Source: the add-in bundled with this plugin
SOURCE_DIR = Path(__file__).resolve().parent / "addin" / "AdomBridge"
# EVERY known Fusion add-in directory convention, newest first. Keep this list
# complete - see the module docstring for why (a single-path assumption caused
# the #63 silent-failure). FusionAddins is created even on a fresh profile.
TARGET_CANDIDATES = [
Path.home() / "AppData" / "Roaming" / "Autodesk" / "FusionAddins",
Path.home() / "AppData" / "Roaming" / "Autodesk" / "Autodesk Fusion" / "API" / "AddIns",
Path.home() / "AppData" / "Roaming" / "Autodesk" / "Autodesk Fusion 360" / "API" / "AddIns",
]
def _sync_directory(source: Path, target: Path):
"""Sync files from source to target, updating only changed files.
- Copies new/modified files from source to target
- Removes files in target that don't exist in source
- Preserves the target directory itself (and any Fusion metadata)
"""
import filecmp
# Build set of relative paths in source
source_files = set()
for src_file in source.rglob("*"):
rel = src_file.relative_to(source)
source_files.add(rel)
dst_file = target / rel
if src_file.is_dir():
dst_file.mkdir(parents=True, exist_ok=True)
elif src_file.is_file():
dst_file.parent.mkdir(parents=True, exist_ok=True)
if not dst_file.exists() or not filecmp.cmp(str(src_file), str(dst_file), shallow=False):
shutil.copy2(src_file, dst_file)
print(f" Updated: {rel}")
# Remove files in target that aren't in source (but keep __pycache__ etc.)
if target.exists():
for dst_file in sorted(target.rglob("*"), reverse=True):
rel = dst_file.relative_to(target)
# Skip __pycache__ and .pyc files
if "__pycache__" in str(rel):
continue
if rel not in source_files:
if dst_file.is_file():
dst_file.unlink()
print(f" Removed: {rel}")
elif dst_file.is_dir() and not any(dst_file.iterdir()):
dst_file.rmdir()
print(f" Removed dir: {rel}")
def find_addins_dir() -> Path | None:
"""Find an existing Fusion AddIns directory, or return the first candidate."""
for candidate in TARGET_CANDIDATES:
if candidate.exists():
return candidate
# Return the first one even if it doesn't exist — we'll create it
return TARGET_CANDIDATES[0]
def install():
"""Install the AdomBridge add-in into EVERY known Fusion add-in location.
ALL of TARGET_CANDIDATES get the add-in (see module docstring - Fusion moved
the scan dir across versions and ignores the others, so single-path installs
silently fail on the "wrong" version). Smart update per target: copies
individual changed files rather than deleting the folder, preserving Fusion's
"Run on Startup" metadata.
NOTE for the AI running this: after install, YOU restart Fusion yourself
(fusion_stop then fusion_start) so it rescans - NEVER ask the user to
restart Fusion; doing things for the user is the entire point of the bridge.
"""
if not SOURCE_DIR.exists():
print(f"ERROR: Source add-in not found at {SOURCE_DIR}")
sys.exit(1)
print(f"Source: {SOURCE_DIR}")
installed_to = []
for addins_dir in TARGET_CANDIDATES:
target = addins_dir / "AdomBridge"
try:
addins_dir.mkdir(parents=True, exist_ok=True)
if target.exists():
_sync_directory(SOURCE_DIR, target)
print(f"Updated: {target}")
else:
shutil.copytree(SOURCE_DIR, target)
print(f"Installed: {target}")
installed_to.append(str(target))
except OSError as e:
print(f" skip {target}: {e}")
print()
print(f"Add-in present in {len(installed_to)} location(s); Fusion loads it from the one "
"its version scans (runOnStartup:true) and ignores the rest.")
print("Restart Fusion via the BRIDGE (fusion_stop then fusion_start) so it rescans - "
"never ask the user to do it. The add-in serves http://127.0.0.1:8774.")
return installed_to
def uninstall():
"""Uninstall the AdomBridge add-in from EVERY known location."""
removed = 0
for candidate in TARGET_CANDIDATES:
target = candidate / "AdomBridge"
if target.exists():
shutil.rmtree(target)
print(f"Removed: {target}")
removed += 1
if not removed:
print("AdomBridge add-in is not installed.")
def main():
if "--uninstall" in sys.argv:
uninstall()
else:
install()
if __name__ == "__main__":
main()