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 102 file(s)
10 files changed
+109−71
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.286+1.9.287
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.286",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.286/adom-bridge-fusion-v1.9.286.zip",- "sha256": "7665f021c00f9ba0d7c12a13f4def1d6836b252fa4ef5bed5bbd2071857443d5",- "size": 578170,+ "version": "1.9.287",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.287/adom-bridge-fusion-v1.9.287.zip",+ "sha256": "5d1a0c8c8a168f5c766fe30d4a20b1cbd3b95fac28f027d5cae7af5bad8a4b06",+ "size": 578906, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.286",+ "version": "1.9.287", "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.9.286"+version = "1.9.287" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.286"+version = "1.9.287" 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.9.286"+DASH_VERSION="1.9.287" 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
library_bundle.py+85−52@@ -78,35 +78,53 @@ def pair_parts(lbr_path: str, models: list, metadata: dict) -> dict: the binding pass covers every device on it). Returns {parts:[{package, lbrPath, modelPath, matchedBy}], unbound:[{package, why}], shared:[{deviceset, package, via}], models}. """- devicesets = [d.get("name") or "" for d in (metadata.get("devicesets") or [])]- packages = {}+ # ONE PART PER (deviceset, PACKAGE), not per deviceset (John, 2026-08-30): a deviceset+ # may carry package VARIANTS (LAN7800 "", "-HV", "-SB" on QFN-48_7x7mm, -HV, -SB), each+ # bound to its own STEP so Change Package swaps footprint and 3D together. The 3D package+ # is per PACKAGE, so every distinct package is its own mint; devicesets sharing a package+ # still share the mint (shared[]).+ units = [] # (deviceset, deviceName, package)+ devicesets = [] for d in metadata.get("devicesets") or []:- pk = ""+ ds = d.get("name") or ""+ devicesets.append(ds)+ seen = set() for v in d.get("variants") or []:- if v.get("package"):- pk = v["package"]- break- packages[d.get("name") or ""] = pk+ pk = v.get("package") or ""+ if not pk or pk in seen:+ continue+ seen.add(pk)+ units.append((ds, str(v.get("device") or ""), pk))+ if not seen:+ units.append((ds, "", ""))+ packages = {ds: next((pk for (d2, _n, pk) in units if d2 == ds and pk), "") for ds in devicesets} by_stem = {_stem(m): m for m in models} by_norm = {_norm(_stem(m)): m for m in models} parts, unbound, used = [], [], set() - def _take(m, how, ds):+ def _take(m, how, ds, pk="", dev=""): used.add(m)- parts.append({"deviceset": ds, "package": packages.get(ds) or ds, "lbrPath": lbr_path,- "modelPath": m, "matchedBy": how})+ parts.append({"deviceset": ds, "device": dev, "package": pk or packages.get(ds) or ds,+ "lbrPath": lbr_path, "modelPath": m, "matchedBy": how}) shared = [] bound_pkgs = {}- for ds in devicesets:- pk = packages.get(ds) or ""+ for (ds, dev, pk) in units: if pk and pk in bound_pkgs: # Several devicesets on ONE package (symbol variants, value clones): the 3D package # is per PACKAGE, so the mint done for the first deviceset serves all of them and the # binding pass writes the urn into every device that uses the package. shared.append({"deviceset": ds, "package": pk, "via": bound_pkgs[pk]}) continue- cands = [(ds, "deviceset"), (pk, "package")] if pk else [(ds, "deviceset")]+ # Match order: package name (the variant's own identity), deviceset+device name+ # (LAN7800-HV), then the bare deviceset (baseline device only).+ cands = []+ if pk:+ cands.append((pk, "package"))+ if dev:+ cands.append((ds + dev, "deviceset+device"))+ else:+ cands.append((ds, "deviceset")) hit = None for name, how in cands: if name in by_stem and by_stem[name] not in used:@@ -125,15 +143,16 @@ def pair_parts(lbr_path: str, models: list, metadata: dict) -> dict: if hit: break if hit:- _take(hit[0], hit[1], ds)+ _take(hit[0], hit[1], ds, pk, dev) if pk: bound_pkgs[pk] = ds else:- unbound.append({"package": ds, "why": "no STEP in the bundle named like the deviceset"- + (" or its package %r" % pk if pk else "")})- if not parts and len(devicesets) == 1 and len(models) == 1:+ unbound.append({"package": pk or ds, "deviceset": ds, "device": dev,+ "why": "no STEP in the bundle named like the package %r" % pk+ + (" or the device %r" % (ds + dev))})+ if not parts and len(units) == 1 and len(models) == 1: unbound.clear()- _take(models[0], "only-one-of-each", devicesets[0])+ _take(models[0], "only-one-of-each", units[0][0], units[0][2], units[0][1]) return {"parts": parts, "unbound": unbound, "shared": shared, "models": models, "modelsUnused": [m for m in models if m not in used]} @@ -190,38 +209,52 @@ def split_per_part(lbr_path: str, out_dir: str) -> dict: sy = g.get("symbol") if sy and sy not in syms: syms.append(sy)- new_root = copy.deepcopy(root)- new_lib = new_root.find(".//library")- for tag in ("packages", "symbols", "devicesets", "packages3d"):- el = new_lib.find(tag)- if el is not None:- new_lib.remove(el)- pe = ET.SubElement(new_lib, "packages")- for pk in pkgs:- if pk in pk_all:- pe.append(copy.deepcopy(pk_all[pk]))- se = ET.SubElement(new_lib, "symbols")- for sy in syms:- if sy in sy_all:- se.append(copy.deepcopy(sy_all[sy]))- de = ET.SubElement(new_lib, "devicesets")- ds2 = copy.deepcopy(ds)- for inst in ds2.iter("package3dinstances"):- pass- for dev in ds2.findall("./devices/device"):- for inst in dev.findall("package3dinstances"):- dev.remove(inst)- de.append(ds2)- # EAGLE order inside <library>: description?, packages, symbols, devicesets- children = list(new_lib)- head = [c for c in children if c.tag not in ("packages", "symbols", "devicesets")]- for c in children:- new_lib.remove(c)- for c in head + [pe, se, de]:- new_lib.append(c)- safe = re.sub(r"[^A-Za-z0-9._-]+", "_", name) or "part"- fp = os.path.join(out_dir, safe + ".lbr").replace("\\", "/")- ET.ElementTree(new_root).write(fp, encoding="utf-8", xml_declaration=True)- out[name] = {"lbrPath": fp, "package": pkgs[0] if pkgs else "", "symbols": syms,- "packages": pkgs}+ # ONE FILE PER (deviceset, PACKAGE) (John, 2026-08-30): a deviceset with package+ # VARIANTS (LAN7800 "", "-HV", "-SB") needs each variant minted on a library whose+ # FIRST (and only) package is THAT variant's footprint, with only the devices that+ # use it. out[deviceset] keeps pointing at the primary package's file for callers+ # that think per deviceset; out[deviceset + "::" + package] is the per-variant key.+ for which, only_pk in [(None, None)] + [(pk, pk) for pk in pkgs]:+ new_root = copy.deepcopy(root)+ new_lib = new_root.find(".//library")+ for tag in ("packages", "symbols", "devicesets", "packages3d"):+ el = new_lib.find(tag)+ if el is not None:+ new_lib.remove(el)+ use_pkgs = [only_pk] if only_pk else pkgs+ pe = ET.SubElement(new_lib, "packages")+ for pk in use_pkgs:+ if pk in pk_all:+ pe.append(copy.deepcopy(pk_all[pk]))+ se = ET.SubElement(new_lib, "symbols")+ for sy in syms:+ if sy in sy_all:+ se.append(copy.deepcopy(sy_all[sy]))+ de = ET.SubElement(new_lib, "devicesets")+ ds2 = copy.deepcopy(ds)+ devs = ds2.find("./devices")+ for dev in list(ds2.findall("./devices/device")):+ for inst in dev.findall("package3dinstances"):+ dev.remove(inst)+ if only_pk and dev.get("package") != only_pk and devs is not None:+ devs.remove(dev)+ de.append(ds2)+ # EAGLE order inside <library>: description?, packages, symbols, devicesets+ children = list(new_lib)+ head = [c for c in children if c.tag not in ("packages", "symbols", "devicesets")]+ for c in children:+ new_lib.remove(c)+ for c in head + [pe, se, de]:+ new_lib.append(c)+ safe = re.sub(r"[^A-Za-z0-9._-]+", "_", name) or "part"+ if only_pk:+ safe += "__" + (re.sub(r"[^A-Za-z0-9._-]+", "_", only_pk) or "pkg")+ fp = os.path.join(out_dir, safe + ".lbr").replace("\\", "/")+ ET.ElementTree(new_root).write(fp, encoding="utf-8", xml_declaration=True)+ entry = {"lbrPath": fp, "package": (only_pk or (pkgs[0] if pkgs else "")),+ "symbols": syms, "packages": use_pkgs}+ if only_pk:+ out[name + "::" + only_pk] = entry+ else:+ out[name] = entry return out
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.286",+ "version": "1.9.287", "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+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.286",+ "version": "1.9.287", "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": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+3−1@@ -13651,7 +13651,9 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict: parts = [] for p in pairing["parts"]: _ds = p.get("deviceset") or p["package"]- _sp = _split.get(_ds) or {}+ # per-variant key first (deviceset::package), then the deviceset's primary file+ _sp = (_split.get(_ds + "::" + (p.get("package") or "")) if p.get("package") else None) \+ or _split.get(_ds) or {} if not _sp.get("lbrPath") or not _sp.get("package"): pairing["unbound"].append({"package": _ds, "why": "could not split this deviceset " "into its own library (no package on its devices?)"})